ご無沙汰してます。豊田です。
随分長いことブログを書いていなかった気がしますが、きっと気のせいですよね。
今回はタイトルにもあるように Rails(ActiveSupport)が提供する Hash#slice! の挙動について、書いていきたいと思います。
いきなりですが問題です
こちらのコードの実行結果、わかりますか?
hash1 = { a: 'hoge', b: 'fuga', c: 'piyo' } hash2 = hash1.slice!(:b) hash2 # => ???
この問題がすぐに(正しく)解ける方は、このブログは読まなくで大丈夫ですw
まずは他の類似メソッドでおさらい
「いきなりそんなこと言われても…」という方もいらっしゃると思いますので、類似のメソッドの挙動をおさらいしてみましょう。
Array#slice(Ruby)
Rubyの組み込みライブラリ、Arrayクラスが提供する #slice メソッドです。(公式)
array1 = [100, 200, 300] array2 = array1.slice(1) array1 # => [100, 200, 300] array2 # => 200
引数で指定したindexに該当する要素を返してくれます。
※他にも異なるパラメータを使う方法がありますが、基本的な動作は変わらないので割愛します
Array#slice!(Ruby)
同じくArrayクラスが提供する #slice! メソッドです。(公式)
array1 = [100, 200, 300] array2 = array1.slice!(1) array1 # => [100, 300] array2 # => 200
レシーバーを破壊的に変更する点は異なりますが、指定した箇所の要素を返すという点では #slice と同じですね。
Hash#slice(Ruby)
続いて同じく組み込みライブラリが提供する Hashクラスの #slice メソッドです。(公式)
hash1 = { a: 'hoge', b: 'fuga', c: 'piyo' } hash2 = hash1.slice(:b) hash1 # => {:a=>'hoge', :b=>'fuga', :c=>'piyo'} hash2 # => {:b=>'fuga'}
指定したkeyと、対応するvalueのペアを返します。
指定した要素を取り出す。という点でArrayクラスの #slice メソッドと似ていますね。
冒頭の問題
それでは、冒頭で出した問題を改めて見てみます。
hash1 = { a: 'hoge', b: 'fuga', c: 'piyo' } hash2 = hash1.slice!(:b) hash2 # => ???
! をつけることでレシーバーを破壊的に変更する違いはあるものの、直感的には Hash#slice と同じ結果になりそうです。
実行してみる
早速実行してみました
ちょっと見づらいので、整形します
hash1 = { a: 'hoge', b: 'fuga', c: 'piyo' } hash2 = hash1.slice!(:b) hash1 # => {:b=>'fuga'} # 上記では実行していませんが、実際にはこうなる hash2 # => {:a=>'hoge', :c=>'piyo'}
どうでしょうか。なんか思ってたのと違いませんか?
指定したkey, valueのペアがレシーバー側に残り、それ以外のkey, valueのペアが削除されています。(そして削除されたペアは、戻り値になっています)
ActiveSupportが提供するHash#slice!
気になったので調べてみたところ、Railsの公式にちゃんと書かれていました。
Replaces the hash with only the given keys. Returns a hash containing the removed key/value pairs.
なるほど。そういう仕様なんですね。
この仕様の違いを私なりに解釈してみると、視点の違いなのかなと思いました。
- Rubyが提供する #slice は、レシーバーから特定の要素を「切り出す」イメージ( => 切り出した側に注目している)
- ActiveSupportが提供する #slice! は、レシーバーから特定の要素以外を「切り落とす」イメージ( => レシーバー側に注目している)
Hash#extract!というメソッドがある
ActiveSupportには Hash#extract! というメソッドが用意されています。
私が Hash#slice! に期待していた挙動は、実はこちらだったりします。
Removes and returns the key/value pairs matching the given keys.
hash = { a: 1, b: 2, c: 3, d: 4 } hash.extract!(:a, :b) # => {:a=>1, :b=>2} hash # => {:c=>3, :d=>4}
なるほど、extract(抽出する)であれば、レシーバーから要素を抽出(取り出す)するというイメージで違和感がありませんね。(抽出した側に注目している)
つまり Hash#slice の破壊的メソッドは Hash#extract! ということになります。
まとめ
- ActiveSupportが提供している Hash#slice! は、その他の #slice, #slice! とは挙動が(直感的に)異なる
- Hash#extract! が(直感的に)他の #slice, #slice! に近い挙動になる
- 普段Rubyのコードを書いているとき「破壊的変更だから ! をつければ良い」と考えていると逆の挙動になるので事故る
いかがだったでしょうか。
私はActiveRecordのenumの実装を読んでいるときに Hash#slice! の挙動を見て違和感を感じ、この仕様を知りました。(もしかしたら、これまでにも無意識に使って事故ってたのかもしれませんが…)
オープンソースのプログラムを読むのは勉強になりますね。