Rubyでのメタプログラミングのことを初めて知った

こんにちは、PDDのドゥックです。仕事の上でsendメソッドをよく使って本当に便利だなと思って、そのようなメソッドをもっと知りたいので、調べてみたところメタプログラミングというものを見つけました。メタプログランミングは広くて難しい領域ですが、Rubyではそれが簡単にできるように、いくつかのメソッドが用意されました。この記事ではそれらのメソッドを紹介したいと思っています。

1. メタプログラミングとは?

簡単にというと、メタプログラミングはコードを書くコードを書くことです。コードでオブジェクト(クラス、 モジュール。。)の中身を変更したり、実行時に新しいメソッドを追加したりできます。

2. 用意されたメソッド

2.1 eval

evalメソッドに渡す文字列をRubyプログラムとして評価してその結果を返します。

例:メソッドを定義する

eval('def hoge; p "Hello World" end')
hoge
#=> "Hello World"

2.2 define_method

渡す文字列をメソッド名としてインスタンスメソッドを定義します。

例:

{hoga: 1, hoge: 2}.each do |key, value|
  define_method key do
    p value
  end
end
hoga
hoge
#=> 1
#=> 2

2.3 send

オブジェクトのメソッド名を引数にして呼び出し、メソッドの実行結果を返します。

例:

class Hoge
  def fuga
    p 'fuga'
  end
  def fuge
    p 'fuge'
  end
end
hoge = Hoge.new
%w(ga ge).each do |key|
  hoge.send("fu#{key}")
end
#=> "fuga"
#=> "fuge"

2.4 method_missing

呼びだされたメソッドが定義されていなかった時、Rubyインタプリタがこのメソッド を呼び出します。デフォルトではこのメソッドは例外NoMethodError を発生させます。method_missingメソッドを再定義した場合、メソッドが見つからない時に欲しい処理を追加することができます。

例:

class Hoge
  attr_accessor :status
  def method_missing(method_name, *arguments)
    if method_name[-1] == '?'
      status == method_name[0..-2]
    else
      super
    end
  end
end
hoge = Hoge.new
hoge.status = 'open'
p hoge.open?
#=> true

2.5 instance_variable_get / instance_variable_set

instance_variable_set:オブジェクトのインスタンス変数に値を設定します。インスタンス変数が定義されていなければ新たに定義されます。

instance_variable_get:オブジェクトのインスタンス変数の値を取得して返します。インスタンス変数が定義されていなければ nil を返します。

例:@hoga, @hogeというインスタンス変数を定義する。

{ga: 1, ge: 2}.each do |key, value|
  instance_variable_set("@ho#{key}", value)
end
p @hoga
p @hoge
#=> 1
#=> 2

{ga: 1, ge: 2}.each do |key, value|
  p "ho#{key}: #{instance_variable_get("@ho#{key}")}"
end
#=> "hoga: 1"
#=> "hoge: 2"

3. 注意

メタプログラミングは魔術のように色々動的な処理に書き換える事が出来ますが、コードの可読性の低下、致命的なバグを生み出す可能性があります。使う時には注意してください。

例えば、2.5では簡単に沢山のインスタンス変数を同時に定義できますが、コードが読み辛くなります。また、普段もし変数がnilなら、ターミナルで変数名が表示されてその名前からエラーのところを見つけることができますね。ですが、こういう定義方法では名前で検索するのが難しいです。大きなプロジェクトなら結構面倒だと思います。

4. 終わりに

上記にいくつかのメソッドを紹介しましたが、メタプログラミング手法はまだまだ沢山あります。今まで、sendだけを使っていましたが、できれば他のメソッドも実際に使用したいと思っています。皆さんもぜひ使ってみてください!