シンボルでも文字列でもアクセス可能なHashを使おう!ActiveSupport {Hash編}
皆さんこんにちは!遅れてきた最年長ルーキー MUGENUP の osada です。
さて皆さんは初めてオープンクラスという概念を知った時、驚かれませんでしたか?私はとても驚きました!だって、基本クラスさえもオーバライド可能なんですよ?
[コラム]オープンクラスとは?
同名でクラス定義を行うと、クラスの再定義(上書き)ではなく、クラスへの追加拡張になる仕組み。
class A def a "a" end end a = A.new raise unless a.a == "a" raise if a.respond_to?(:b) # b というメソッドはない # 同名のクラス定義は拡張になる class A def b "b" end end raise unless a.b == "b" raise unless a.respond_to?(:b) # b というメソッドがある # a というメソッドも維持される raise unless a.respond_to?(:a) raise unless a.a == "a"
このオープンクラス、String
など基本クラスさえも拡張可能なのが凄いです。Javascript の prototype みたいですよね。
ActiveSupport コア拡張
オープンクラスという特徴的な仕組みのおかげで、Rails では基本クラスなのに便利なメソッドが沢山ある、という状態になっています。それがActiveSupportのコア拡張です。
ということで、今日のお題はActiveSupport
のHash
です。ターゲットはRails初心者の方。一緒に勉強できれば嬉しいです。
Hash 編
hash拡張のソースは、ここにありました。9ファイル、19メソッドあります。
ruby/2.0.0/gems/activesupport-3.2.16/lib/active_support/core_ext/hash
conversion.rb
まずは Hash 変換するconversion.rb
です。といっても対象はxml。そういえばそんなのもありましたね!
{"foo" => 1, "bar" => 2}.to_xml
できそうな気がしてましたよね?
Hash.from_xml(<<"EOS" <?xml version="1.0" encoding="UTF-8"?> <hash> <foo type="integer">1</foo> <bar type="integer">2</bar> </hash> EOS)
できそうな気がしてましたよね?
deep_dup.rb
ruby の dup
, clone
はシャローコピーだというのはよく知られています。
シャローコピー?という方はRubyist Magazine - 値渡しと参照渡しの違いを理解するがオススメ
deep_dup は深いコピーをしてくれます。当然再帰ですよね〜。
def deep_dup duplicate = self.dup duplicate.each_pair do |k,v| tv = duplicate[k] duplicate[k] = tv.is_a?(Hash) && v.is_a?(Hash) ? tv.deep_dup : v end duplicate end
ところで、duplicate[k]
とv
の値が異なることがあるのでしょうか?
deep_merge.rb
深いコピーがあるなら、深いマージもあるでしょう。そうでしょう。
こちらで興味深いのは、破壊メソッドを作っておいて、それを dup
と組み合わせれば通常のメソッドになるということです。センスありますね。
def deep_merge(other_hash) dup.deep_merge!(other_hash) end # Same as +deep_merge+, but modifies +self+. def deep_merge!(other_hash) other_hash.each_pair do |k,v| tv = self[k] self[k] = tv.is_a?(Hash) && v.is_a?(Hash) ? tv.deep_merge(v) : v end self end
diff.rb
Hash の差分って、どうされてますか? Array なら マイナス[1,2] - [2,3] # => [1]
が使えますが、Hash にはありません。ここでdiff
の出番です
しかしこのdiff
、 差分ではなく、差異なんです。
[6] pry(main)> { 1 => 2 }.diff({1 => 2, 2 => 2, 3 => 2}) => {2=>2, 3=>2}
2つのハッシュの相違点を取り出すメソッドなんですね。実装はとてもシンプルで、ハッシュ1とハッシュ2に同じkey
を見つけてvalue
も同じときは消す。ハッシュ2からハッシュ1にあるkeyを消す。そしてマージです。
def diff(h2) dup.delete_if { |k, v| h2[k] == v }.merge!(h2.dup.delete_if { |k, v| has_key?(k) }) end
なんと悲しいことに、Rails4 では Deprecated になっていました!
def diff(other) ActiveSupport::Deprecation.warn "Hash#diff is no longer used inside of Rails, and is being deprecated with no replacement. If you're using it to compare hashes for the purpose of testing, please use MiniTest's assert_equal instead." dup. delete_if { |k, v| other[k] == v }. merge!(other.dup.delete_if { |k, v| has_key?(k) }) end
Hash#deep_diff
, the recursive difference between two hashes by nikitug · Pull Request #8142 · rails/rails
deep_diff
を使えばいいじゃん、ということですが、まだ時期ではない、ということで、deprecated
を付けるだけになっているようです。しばらくは、diff
ということでしょうか。
with_indifferent_access.rb
params がシンボルでも文字列でもアクセスできて、不思議だなぁ?と思われたことありませんか? 普通は nil が返ってきます。
[8] pry(main)> h = {a: 1, "b" => 2} => {:a=>1, "b"=>2} [9] pry(main)> h[:a] => 1 [10] pry(main)> h["a"] => nil [11] pry(main)> h[:b] => nil [12] pry(main)> h["b"] => 2
そんな時に使うのか、with_indifferent_accessです!
[13] pry(main)> h = {a: 1, "b" => 2}.with_indifferent_access => {"a"=>1, "b"=>2} [14] pry(main)> h[:a] => 1 [15] pry(main)> h["a"] => 1 [16] pry(main)> h[:b] => 2 [17] pry(main)> h["b"] => 2
このメソッドは、HashWithIndifferentAccess
へのプロキシメソッドです。
def with_indifferent_access ActiveSupport::HashWithIndifferentAccess.new_from_hash_copying_default(self) end
それが何かと言いますと、
module ActiveSupport class HashWithIndifferentAccess < Hash
Hash のサブクラスでした。そして、new_from_hash_copying_default(self)
は
def self.new_from_hash_copying_default(hash) new(hash).tap do |new_hash| new_hash.default = hash.default end end
となっていますので、やっていることは、元 hash の default を自身に適用しているだけです。まさに継承している感じですね。
ではコンストラクタに秘密があるのでしょうか?
def initialize(constructor = {}) if constructor.is_a?(Hash) super() update(constructor) else super(constructor) end end
この場合、super
はHash
クラスの呼び出しですから、ミソはupdate
にあるようです。あれ?update
ってマージするメソッドだったような?
def update(other_hash) if other_hash.is_a? HashWithIndifferentAccess super(other_hash) else other_hash.each_pair { |key, value| regular_writer(convert_key(key), convert_value(value)) } self end end
ハッシュが既にHashWithIndifferentAccess
だった場合は、そのままインスタンス化するだけです。するとelse
が心臓部のようです。
other_hash.each_pair { |key, value| regular_writer(convert_key(key), convert_value(value)) }
文字列でもシンボルでも
key 側の処理はこんな感じ。
def convert_key(key) key.kind_of?(Symbol) ? key.to_s : key end
単に Symbol だったら文字列に変換しているんですね。ずるい!なんてシンプルな実装なんでしょうか。
このconvert_key
を各所で使うため、key は全てシンボルではなく文字列になります。
でしたよね?
[13] pry(main)> h = {a: 1, "b" => 2}.with_indifferent_access => {"a"=>1, "b"=>2}
convert_value
value側の処理です。
def convert_value(value) if value.is_a? Hash value.nested_under_indifferent_access elsif value.is_a?(Array) value.dup.replace(value.map { |e| convert_value(e) }) else value end end
1 . value が Hash だったときは、indifferent_access に変換して返します。 というか単にエリアスが貼ってあるだけでした。
alias nested_under_indifferent_access with_indifferent_access
普通に
value.with_indifferent_access
と書いても良い所をあえてvalue.nested_under_indifferent_access
と書く所、こだわっていますね。
2 . value がArray のとき、
elsif value.is_a?(Array) value.dup.replace(value.map { |e| convert_value(e) })
再帰して返すというお決まりのコードです。ところでなぜ
value.map { |e| convert_value(e) }
ではいけないのでしょうか?
これはおそらく、is_a?
は親クラスであっても true になるからだと思います。
value が Array のサブクラスだった場合、`value.map { |e| convert_value(e) }
では、サブクラスではなく、Arrayが返ってしまいますからね。
ということで、心臓部はこの2つのメソッドで、後は全て、conver_*
を経由するように書き換えているだけでした。例えば、key?
は convert_key
を経由するように書き換えているだけです。
def key?(key) super(convert_key(key)) end
シンボルでも文字列でもアクセスできる、というのは key
と value
に対して、根本のところで文字列に変換してしまう、という方法で実装されていました。
これさえ理解しおけば、シンボルでも文字列でも扱えるHashが使いこなせますね!
except.rb
Hash の中から、特定の key だけを消したいこと、結構ありますよね?これは有名なので皆さんご存知かと思います。
[12] pry(main)> {a: 1, b: 2}.except(:a, :c) => {:b=>2}
def except(*keys) dup.except!(*keys) end # Replaces the hash without the given keys. def except!(*keys) keys.each { |key| delete(key) } self end
keys.rb
Hash の key に対するメソッド群です。
- stringfy_keys
def stringify_keys dup.stringify_keys! end # Destructively convert all keys to strings. Same as # +stringify_keys+, but modifies +self+. def stringify_keys! keys.each do |key| self[key.to_s] = delete(key) end self end
delete で取り出しながら、key を変換して格納し直すという、 エレガントな入れ替えですね。
- 逆にシンボル化もできるようです。
def symbolize_keys dup.symbolize_keys! end # Destructively convert all keys to symbols, as long as they respond # to +to_sym+. Same as +symbolize_keys+, but modifies +self+. def symbolize_keys! keys.each do |key| self[(key.to_sym rescue key) || key] = delete(key) end self end
(key.to_sym rescue key)
とか、シンプルでかっこいいですね~。
そういえば、to_options とかご存知でした?
alias_method :to_options, :symbolize_keys alias_method :to_options!, :symbolize_keys!
- assert_valid_keys で key の確認を便利にできます。
def assert_valid_keys(*valid_keys) valid_keys.flatten! each_key do |k| raise(ArgumentError, "Unknown key: #{k}") unless valid_keys.include?(k) end end
reverse_merge.rb
渡した方の hash にマージするメソッドです。
self.reverse_merge(other_hash)
ではなく
other_hash.merge(self)
でいいんじゃないの?
ところが、面白い箇所が1点!
merge!( other_hash ){|key,left,right| left }
って何ですか?
def reverse_merge(other_hash) other_hash.merge(self) end # Destructive +reverse_merge+. def reverse_merge!(other_hash) # right wins if there is no left merge!( other_hash ){|key,left,right| left } end alias_method :reverse_update, :reverse_merge!
同じkeyがあったとき、左側(self)を優先にできるようです。
[24] pry(main)> {a: 1, b: 2}.reverse_merge!({a: 1, c: 3}) => {:a=>1, :b=>2, :c=>3} [25] pry(main)> {a: 1, b: 2}.reverse_merge({a: 1, c: 3}) => {:a=>1, :c=>3, :b=>2} [26] pry(main)> {a: 1, b: 2}.merge({a: 1, c: 3}) => {:a=>1, :b=>2, :c=>3} [27] pry(main)> {a: 1, b: 2}.merge!({a: 1, c: 3}) => {:a=>1, :b=>2, :c=>3}
破壊型のときは、自分に取り込むよ! ということで自分が優先なんでしょうか? merge に block が渡せるのは、初めて知りました。勉強になります
slice.rb
特定のkey だけを取り出したいこと、よくありますよね?
[15] pry(main)> a = {a: 1, b:2} => {:a=>1, :b=>2} [16] pry(main)> a.slice(:a, :c) => {:a=>1} [17] pry(main)> a => {:a=>1, :b=>2}
def slice(*keys) keys = keys.map! { |key| convert_key(key) } if respond_to?(:convert_key, true) hash = self.class.new keys.each { |k| hash[k] = self[k] if has_key?(k) } hash end
特定のkeysだけ取り出して、それ以外は返り値で返すとか、delete っぽくでオシャレ!Hashの分割で使えますね。
[18] pry(main)> a.slice!(:a, :c) => {:b=>2} [19] pry(main)> a => {:a=>1}
def slice!(*keys) keys = keys.map! { |key| convert_key(key) } if respond_to?(:convert_key, true) omit = slice(*self.keys - keys) hash = slice(*keys) replace(hash) omit end
逆に特定のkeys だけを返してくれるメソッドもありました。
[20] pry(main)> a = {a: 1, b:2} => {:a=>1, :b=>2} [21] pry(main)> a.extract!(:a, :c) => {:a=>1} [22] pry(main)> a => {:b=>2}
def extract!(*keys) result = {} keys.each {|key| result[key] = delete(key) } result end
まとめ
ということで、何か新しい発見はあったでしょうか?
9ファイル19メソッドのHash拡張でした。
with_indifferent_access, deep_dup, except, slice, slice!
辺りは、どんどん使っていきたいと思います。
便利なメソッドがあったら、是非教えて下さい!
宣伝
MUGENUP では、rails を使いたいエンジニアを募集中です。 無限流開発、ご一緒しませんか?
大きな裁量で自社サービス開発!Rubyエンジニアをウォンテッド! - 株式会社MUGENUPの求人 - Wantedly