Ruby
RubyDay 19

Rubyでメタプログラミングことはじめ

More than 3 years have passed since last update.

コードを書くコードを書く、それが所謂メタプログラミングと呼ばれるものですが、Rubyではメタプログラミングが簡単にできるようなメソッドがいくつも用意されています。今回はその事始めとして幾つかパターンを学びつつ書いていきます。


evalを使う

evalは引数としてわたされるStringオブジェクトをRubyのコードとして評価する。

メタプログラミングをする時よく用いられる。

eval '1 + 2' # => 3


動的にインスタンス変数を定義する時

def initialize(opts = {})

opts.each do |key, value|
eval "@#{key}=value"
end
end


動的にメソッドを定義する時

class Hoge

%w(sample1 sample2 sample3).each do |str|
eval <<-EOS
def
#{str}
# something
end
EOS
end
end


define_methodを使う

クラスやモジュールに引数として与えたメソッド名から新しいメソッドを定義することが出来ます。

class Hoge

%w(sample1 sample2 sample3).each do |str|
define_method str do
# something
end
end
end


class_eval/module_eval/instanse_evalを使う

evalと違い、評価する時の対象となるクラスやインスタンスを指定して評価することができます。

class Hoge

end

a = Hoge.new
b = Hoge.new

a.instance_eval do
def sample1
"called"
end
end

Hoge.class_eval do
def sample2
"sample2 called"
end
end

a.sample1 # => "called"
b.sample1 # => NoMethodError: undefined method ...

a.sample2 # => "sample2 called"
b.sample2 # => "sample2 called"


動的なメソッドの呼び出し

Object#sendを使う。

レシーバの持つメソッドを呼び出すことができる。

第二引数で呼び出したメソッドに与える引数を渡す。

# 1 + 2

1.send(:+, 2) # => 3

3.send(:*, 5) # => 15


method_missingを使う

Rubyの継承チェーンからメソッド探索を繰り返した時、どこにも指定したメソッドが見つからない場合に元のレシーバであるオブジェクトのmethod_missingを呼び出す。

NoMethodErrorを返すのは、多くの場合このmethod_missingによるものである。

class Hoge

def method_missing(method, *args)
puts "call #{method}(#{args.join(',')})"
puts "block is given" if block_given?
yield
end
end

hoge = Hoge.new
hoge.sample_task('a', 'b') do
puts "sample task"
end

# => call sample_task(a,b)
# block is given
# sample task


ゴーストメソッド

method_missingをオーバーライドすることで、実際には存在しないメソッドを呼び出したかのような挙動を見せることができる。

これはゴーストメソッドとよばれる。

標準クラスであるOpenStructを例にとってみよう。

require 'openstruct'

ice = OpenStruct.new
ice.flavor = "strawberry"
ice.flavor # => "strawberry"

また、このような動作は単純なものであれば簡単に実装できる。

class MyStruct

def initialize
@attributes = {}
end

def method_missing(name, *args)
attribute = name.to_s
if attribute =~ /=$/
@attributes[attribute.chop] = args[0]
else
@attributes[attribute]
end
end
end

ice = MyStruct.new
ice.flavor = 'strawberry'
ice.flavor # => 'strawberry'


文字列から既存のメソッドを検索する時

新しくメソッドを作るとき既存のメソッドと衝突させないために、予め定義されているメソッドを調べたい時等、文字列からメソッドが定義されているか調べるとき

Object#methodsとEnumerable#grepを使う。

grepは、引数として与えたRegexpクラスのオブジェクトとの===、正規表現のマッチでtrueとなる文字列を返します。

Object.methods.grep(/se/)

# => [:const_set, :class_variable_set, :itself, :instance_variable_set, :send, :public_send, :__send__]


参考資料

パーフェクトRuby (PERFECT SERIES 6) Rubyサポーターズ

メタプログラミングRuby Paolo Perrotta