Rubyのハッシュにメソッドでアクセスするには?やらない方がいい?

JavaScriptな脳状態でRubyを触り始めると、ハッシュにアクセスする時にエラーで怒られる。

h={abc:123}
=> {:abc=>123}

h.abc
NoMethodError: undefined method `abc' for {:abc=>123}:Hash
	from (irb):15
	from /usr/bin/irb:12:in `<main>'


いかん、いかん、Rubyのハッシュはメソッド呼び出しではなく、[:abc]のように指定するのだ。

h[:abc]
=> 123


Rubyのハッシュは、キーとメソッドを明確に区別することで、キーの自由度はとっても広がった。

  • メソッド名と同じキーも使えるし、
  • シンボルや文字列に限らず、あらゆるオブジェクトがキーとなる。
irb(main):022:0> require 'date'
=> true

irb(main):023:0> date=Date.parse("2014-11-23")
=> #<Date: 2014-11-23 ((2456985j,0s,0n),+0s,2299161j)>

irb(main):024:0> h[date]="勤労感謝の日"
=> "勤労感謝の日"

irb(main):025:0> h
=> {:abc=>123, #<Date: 2014-11-23 ((2456985j,0s,0n),+0s,2299161j)>=>"勤労感謝の日"}


一方、オブジェクトは区別される。よって、シンボルと文字列も区別される。忘れると謎のnilに悩む...。

irb(main):027:0> h={'abc'=>123}
=> {"abc"=>123}

irb(main):028:0> h[:abc]
=> nil

irb(main):029:0> h['abc']
=> 123
  • シンボルキーと文字列キーがごちゃ混ぜのハッシュとならないように、事前に決断しておきたい。
  • ところが、せっかくシンボルキーに決断したのに、利用するライブラリは文字列キーを返したり、
  • 世の中、なかなか思いどおりにはいかない...。


いっそのこと、キーの自由度は狭まるけれど、場合によってはメソッドアクセスできた方が幸せを感じるかもしれない、と思い始めた。

メソッドアクセスの仕様

  • 良きに計らい、シンボルキーでも、文字列キーでも、メソッドアクセスできるようにしたい。
  • そうは言っても、h={'abc'=>123, :abc=>456}のようなハッシュも存在するので、どちらのキーを優先するのか決めておく必要もある。
    • 最近のハッシュ表現は、シンボルキーが多い。
irb(main):033:0> h={abc:123}
=> {:abc=>123}
    • よって、シンボルキーを優先することにした。
  • 読み出しだけでなく、書き込みもできるようにしておきたい。

コード

  • refineで拡張しているので、以下のコードを書いたファイル内でのみ、ハッシュへのメソッドアクセスが可能になる。
  • method_missingを利用しているので既存メソッドが優先される。
  • よって、メソッド名と同じキーには、メソッドアクセスできない。
    • 但し、従来の[キー]アクセスは可能。
module HashEx
  refine Hash do
    def method_missing(method, *args)
      if method[-1] == '='
        string = method[0..-2]
        symbol = string.to_sym
        key?(symbol) ? self[symbol] = args[0] : key?(string) ? self[string] = args[0] : self[symbol] = args[0]
      else
        key?(method) ? self[method] : self[method.to_s]
      end
    end
  end
end
using HashEx

実験

> h              # => {"abc"=>123, :abc=>456}
> h.abc          # => 456
> h.abc = 999    # => 999
> h              # => {"abc"=>123, :abc=>999}
> h.delete(:abc) # => 999
> h              # => {"abc"=>123}
> h.abc = 666    # => 666
> h              # => {"abc"=>666}
> h.xyz = 789    # => 789
> h              # => {"abc"=>666, :xyz=>789}
  • 読み出し
    • メソッド名と同じ値のシンボルキーを優先して読み出す。
    • シンボルキーが見つからない時は、文字列キーを読み出す。
  • 書き込み
    • メソッド名と同じ値のシンボルキーに優先して上書きする。
    • シンボルキーが見つからない時は、文字列キーに上書きする。
    • シンボルキーも文字列キーも見つからない時は、新たなシンボルキーに書き込みする。

活用した時のコード風景の違い

キーアクセス(従来)

メソッドアクセス

  • メソッドアクセスの方が、タイプ数が2文字分少ないので、若干コードが短くなる。
  • メソッドアクセスの方が、.をタイプするだけなので、入力時の負担が少ない。
    • 但し、最近のコードエディタを使えば補完機能が優秀なので、ほとんど差はないかもしれない。
  • 逆にメソッドアクセスにしてしまうと、他人がコードを見た時に、ハッシュ内の値を取り出したいのか、メソッドで操作したいのか、分かりにくくなる。
  • シンボルに対しては特定の強調されたシンタックスハイライトを設定可能なので、コードを読む時は従来のキーアクセスの方が読みやすい気がした。

もう一つのメソッドアクセス

  • ハッシュへのメソッドアクセスのことを調べていると、OpenStructという標準ライブラリがあることに気付いた。
  • OpenStructはハッシュではないのだけど、メソッドアクセス可能なキーと値の要素を内部に保持するオブジェクト。
irb(main):085:0> require 'ostruct'
=> true

irb(main):068:0> os=OpenStruct.new
=> #<OpenStruct>

irb(main):069:0> os.abc
=> nil

irb(main):070:0> os.abc=123
=> 123

irb(main):071:0> os.abc
=> 123

irb(main):072:0> os.abc=999
=> 999

irb(main):073:0> os.abc
=> 999

irb(main):074:0> os
=> #<OpenStruct abc=999>

irb(main):075:0> os.delete_field(:abc) # 引数'abc'を指定してもOK
=> 999

irb(main):076:0> os.abc
=> nil

irb(main):077:0> os
=> #<OpenStruct>
  • ハッシュからOpenStructを生成することもできる。
irb(main):081:0> h={abc:123, efg:456}
=> {:abc=>123, :efg=>456}

irb(main):082:0> os=OpenStruct.new(h)
=> #<OpenStruct abc=123, efg=456>
  • シンボルキーと文字列キーの文字表現が同じ場合、最後に評価されたキーが優先されるようだ。
    • 但し、ハッシュ内部でキーの順序がどのように評価されるか不明。(自分は知らない)
    • よって、ハッシュを利用しているうちに、予想外に順序が変化することもあるかも?
irb(main):083:0> os=OpenStruct.new({:abc=>111, 'abc'=>999})
=> #<OpenStruct abc=999>

irb(main):084:0> os=OpenStruct.new({'abc'=>999, :abc=>111})
=> #<OpenStruct abc=111>
      • eachは使えないけど、each_pair(Ruby2.0以降)が使える。
irb(main):001:0> require 'ostruct'
=> true

irb(main):002:0> os=OpenStruct.new({abc:123, efg:456})
=> #<OpenStruct abc=123, efg=456>

irb(main):003:0> os.each {|i| p i}
=> nil

irb(main):004:0> os.each_pair {|i| p i}
[:abc, 123]
[:efg, 456]
=> {:abc=>123, :efg=>456}
      • to_h(Ruby2.0以降)するとシンボルキーのハッシュを生成する。
      • よって、必要に応じてto_hすれば、ハッシュと同等に使えるのだ。
      • eachだって、こうすれば使える。
irb(main):013:0> os.to_h.each {|i| p i}
[:abc, 123]
[:efg, 456]
=> {:abc=>123, :efg=>456}

Railsのハッシュ拡張

irb(main):001:0> require 'active_support/all'
=> true
  • requireだけでは、ハッシュは従来どおりの機能である。
  • シンボルキーと文字列キーは、明確に区別される。
irb(main):002:0> h={abc:123, efg:456}
=> {:abc=>123, :efg=>456}

irb(main):003:0> h
=> {:abc=>123, :efg=>456}

irb(main):004:0> h[:abc]
=> 123

irb(main):005:0> h['abc']
=> nil
  • 一方、with_indifferent_accessというハッシュの拡張があって、(indifferent=無頓着な、どっちでもかまわない)
  • with_indifferent_accessで生成したハッシュは、シンボル・文字列どちらのキーでもアクセス可能になるのだ!
irb(main):006:0> h=h.with_indifferent_access
=> {"abc"=>123, "efg"=>456}

irb(main):007:0> h
=> {"abc"=>123, "efg"=>456}

irb(main):008:0> h['abc']
=> 123

irb(main):009:0> h[:abc]
=> 123
  • with_indifferent_accessは、元のハッシュから、すべて文字列キーのハッシュを生成する。
  • そして、生成されたindifferentな文字列キーのハッシュは、シンボルキーでもアクセス可能になるのだ!
    • with_indifferent_access!という破壊的メソッドはないので、同じ変数に代入している。
  • いや、そもそも途中でハッシュの性質を変えるのはナンセンス。本来は、最初の定義でwith_indifferent_accessを指定しておくべきなのだと思う。
irb(main):015:0> h={abc:123, efg:456}.with_indifferent_access
=> {"abc"=>123, "efg"=>456}

素晴らしい!

  • 結局、自分が求めていたのはメソッドアクセス可能なハッシュではなくて、indifferentなハッシュなのだと思う。
  • シンボルキーと文字列キーが混在してしまう時の苦労をしたくない、という欲求が脳内の根底にあった。
  • JavaScript脳の状態でRubyを使いだすと、メソッドアクセスできないことに不便を感じたが、それは自分自身がRubyモードになるだけで解決できる。
  • 後のコードの読みやすさを考えれば、キーによるハッシュ要素へのアクセスなのか、メソッドによるハッシュ操作なのか、区別できた方が読み易いはず。
  • たった2文字の節約のために、Rubyらしいコード表現を隠してしまうのは、改善ではなく、改悪に向かっていた。

教訓:基本クラスのオレオレ拡張より、先達の知恵。