Help us understand the problem. What is going on with this article?

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away