ハッシュを構造体のように使う

この記事は、Ruby Advent Calendar jp: 2009参加記事です。前日はudzuraさんの「Ruby1.8.7、1.9.1、1.9.2preview1を簡単に切り替える@Ubuntu9.10」でした。明日はdan5.yaさんです。

やりたいこと

Rubyの構造体 (Struct) では、以下のようにメソッド呼び出しの形で構造体メンバにアクセスすることができます。

Dog = Struct.new(:name, :age)
pochi = Dog.new("pochi", 3)
pochi.age #=> 3

一方、ハッシュを使う場合、値を取り出すためには [] の中にハッシュキーを入れる形にする必要があります。

pochi = {:name => "pochi", :age => 3}
pochi[:age] #=> 3
puts pochi.age #=> NoMethodError

この書き方だと必ずしも見た目的・タイプ量的に好ましくないので、ハッシュの要素に対して構造体メンバと同じような形でアクセスできないか? というのがこの記事の目的です。

実現手段

安直な方法としては、Hashクラスに method_missing を定義して、不明なメソッドの呼び出しをハッシュ要素の参照と扱うことが考えられます。

class Hash
  def method_missing(name, *args)
    self[name]
  end
end

pochi = {:name => "pochi", :age => 3}
puts pochi.age #=> 3

簡単に解説すると、ハッシュオブジェクトに対して未定義のメソッド (ここではage) が呼び出されると、Hash#method_missingが呼び出されます。ここで、method_missingの第1引数にはメソッド名がシンボルとして入るので、この例ではname = :age となります。結果として、hash.age は hash[:age] と等価になります。

ただし、この方法では、Hashクラスの挙動をグローバルに変更してしまうので、ハッシュを使う既存のRubyコードに意図しない副作用を生じる危険性があります。

ハッシュ拡張の影響を局所化する手段として、Hashのサブクラスを定義する方法と、特異クラスを使う方法がありえます。このうちサブクラス定義はうまい方法が見つからない (特に通常のハッシュオブジェクトからの変換) ので、ここでは特異クラスを使う方法を取り上げます。

特異クラスとは、特定のオブジェクトに対してメソッドやインスタンス変数を動的に定義することを指します。具体的には、以下のようなコードになります。

class Hash
  def structize!
    # 自分自身に対する特異クラス定義
    class << self
      def method_missing(name, *args)
        self[name]
      end
    end
  end
end

pochi = {:name => "pochi", :age => 3}
puts pochi.age #=> NoMethodError
pochi.structize!
puts pochi.age #=> 3

上記のコード例で、class << self から対応するendまでの部分が特異クラス定義です。この場合、structize! メソッドのレシーバであるハッシュオブジェクトに対して、method_missing を動的に定義することになります。普通にハッシュオブジェクトを作っただけでは method_missing が定義されず、ハッシュオブジェクトに対して structize! を呼び出したときに限り有効になることがポイントです。

このコード例では最低限の処理しかしていないので、実用上は、キーが文字列である場合や、キーが存在しない場合の例外送出などの処理を追加する必要があります。

method_missingと特異クラス定義の組み合わせによるハッシュの拡張については、Twitterクライアントライブラリ Rubytter のコードを参考に (というかメソッド名を含めて全面的に利用) させていただきました。