builder by ZDNet Japanをご愛読頂きありがとうございます。

builder by ZDNet Japanは2022年1月31日にサービスを終了いたします。

長らくのご愛読ありがとうございました。

Rubyによるメタプログラミング

文:Steve Hayes(Builder AU) 翻訳校正:石橋啓一郎
2007-12-13 09:00:00
  • このエントリーをはてなブックマークに追加

 この場合、newメソッドを呼び出す際に、所有者の配列がどこから始まるかを明示的に示す必要はない。メソッドの定義の"*owners"は、残りの引数はすべて"owners"と呼ばれる配列に代入するということを示している。この可変長引数配列のデフォルト値は、明示はされていないが空の配列([])となっている。可変長配列引数に明示的にデフォルト値を与えると、エラーとなる。

 どんなメソッドの定義にも、オプションで別の形の引数を1つ含めることができる。メソッド内で呼び出すことのできるコードのブロックだ。これは、前回のコラムの中で挙げた列挙法(Enumeration Method)の例ですでに触れられている。

(1..5).each { |n| puts n*n }

 このコードの{}はオプションのブロックを表している。この方法を使ってどのようにメソッドを定義できるかを示すために、ある本がブロックの中に含まれる一定の条件式に一致するかどうかを示すブール値を返すメソッドを、Bookクラスに1つ追加してみよう。メソッドの呼び出し方は例えば次のようになる。

book.matches { |book| book.title == 'War and Peace' }

 そして、実装は次のようになる。

class Book

  # accessors and other details omitted

  def matches(&block)
    yield(self)
  end

end

 &blockの"&"は、これがオプションのブロック引数であることを意味しており、ここでのyieldメソッドは、selfをパラメタとしてオプションブロックパラメタに制御を引き継ぐを意味している。次に示す2つのコードは論理的には等価で、同じことを行う別の方法の例だ。

class Book

  # accessors and other details omitted

  def matches(&block)
    block.call(self)
  end

end

book.matches do |book| 
book.title == 'War and Peace'
end

 Rubyにはコードの表現力を増すトリックがもう2つある。まず、メソッド呼び出しの際の括弧は通常省略可能だ。プログラマーは括弧、角括弧、波括弧の蔓延には慣れているが、これらの文字は自然言語ではあまり使われず、これらを取りのぞけばコードはもっと読みやすくなる。さらに、Rubyはハッシュをメソッドの引数に使う際の取り扱いが柔軟だ。

 思い出して欲しいのだが、ハッシュはキーと値の組の集合であり、ハッシュのリテラルは次のようになる。

{ :first_name => ‘Nelson’, :last_name => ‘Mandela’ }

 ハッシュを使うことによって、パラメタに名前を付けることができる。Bookクラスを修正して、新しいインスタンスを生成する際に、著者とISBNに別のパラメタを使うのではなくハッシュを使うようにしてみよう。

class Book

def initialize(title, details = {}, *owners)
@title = title
@author = details[:author] || 'Unknown'
@isbn = details[:isbn]
@owners = owners
end

end

book = Book.new('War and Peace', 
                             {:author => 'Tolstoy', :isbn => '0375760644'}, 
                             'Steve', 'Amanda', 'Marty')

 ここではハッシュをリテラルで定義している。ここでも、必要のない引数を指定する必要はなく、ハッシュ内部では好きな順序で引数を定義することができ、ハッシュのキーを使うことでメソッドの呼び出しをより表現豊かにすることができる。配列に読み込む必要のある可変長のオプション引数がなければ、Rubyでは{}の区切り文字も使う必要がない。メソッド呼び出しの最後までキーと値の組を読み、それをハッシュに入れる。従って、次のような表現も正しいコンストラクタとなる。

book = Book.new('War and Peace’, :author => 'Tolstoy', :isbn => '0375760644’)

 ただし、前述の機能をすべて同時に使おうとすると、制約が生じる。オプションブロックは最後のパラメタでなくてはならない。可変長のオプション配列は、オプションブロック以外のパラメタの最後になければならない。キーと値の組が自動的にハッシュとして認識されるのは、これらがメソッド呼び出しの最後のパラメタである場合だけだ。以下の1番目呼び出しの例は、キーと値の組が最後のパラメタであるため正しいが、2番目の呼び出しの例は、他のパラメタが後ろに続くため正しくなく、Rubyはどれを配列として読み込み、どれをハッシュとすべきかを判断できない。

book = Book.new( 'War and Peace’, 
                              :author => 'Tolstoy',
                              :isbn => '0375760644’) # legal!

book = Book.new( 'War and Peace’, 
                              :author => 'Tolstoy', 
          :isbn => '0375760644’, 
                              ‘Steve’, ‘Amanda’, ‘Marty’) # not legal!

 これらの機能を使うと、柔軟で表現に富んだメソッド定義と呼び出しを行うことができるが、Rubyは他にもいくつかDSLの作成に便利な機能を持っている。eval、class_eval、そしてinstance_evalだ。

 基本的に、evalは文字列を取ってそれをRubyのステートメントとして評価する。(evalはそれが実行されるコンテキストでの評価を行うことができるが、このコラムでは触れないことにする。)evalはカーネルのメソッドであり、オブジェクト内部や単純なスクリプトの中でも使うことができる。

 例を挙げてみよう。

eval “2 + 2” # => 4

 これは、動的に作成される任意の文字列、あるいはファイルなどの外部から読み込まれる任意の文字列を実行したい場合に便利だ。

 instance_evalメソッドは、Objectのパブリックメソッドで、文字列1つあるいはブロック1つをパラメタとして取り、そのパラメタをレシーバーのコンテキストで実行する。文字列あるいはブロックが実行されると、selfはinstance_evalのレシーバーに設定される。これは、ブロック内部からインスタンスの変数とプライベートメソッドにアクセスできるということを意味しているが、これはレシーバーのカプセル化を破ってしまうため、簡潔だが危険だ。では、instance_evalを使った本のマッチングをするコードを見てみよう。


class Book

def initialize(title, details = {}, *owners)
@title = title
@author = details[:author] || 'Unknown'
@isbn = details[:isbn]
@owners = owners
end

  def matches(&block)
    self.instance_eval &block
  end

end

book = Book.new('War and Peace', 
                             {:author =>'Tolstoy', :isbn => '0375760644'}, 
                             ['Steve', 'Amanda', 'Marty'])

book.matches {@title == 'War and Peace'} # => true

 instance_evalのもう1つのよくある使い方は、実行時に新しいメソッドを定義することだ。Rubyでは、インスタンスごとに個別のメソッドを定義することができ、これはinstance_evalを使ってメソッドを定義し、レシーバーがインスタンスである場合に起こる。


book = Book.new('War and Peace', 
                             {:author =>'Tolstoy', :isbn => '0375760644'}, 
                             ['Steve', 'Amanda', 'Marty'])

book.instance_eval do
  def location
    "Russia"
  end
end

book.location # => "Russia"

book2 = Book.new('Lord of the Rings', 
                {:author =>'Tolkien'}, 
                ['Jonathan'])

book2.location # => undefined method `location'

 クラスに対して同じことをする場合も、同じルールが適用される。この場合、レシーバーは名前を持つClassのインスタンスで、新しく定義されるメソッドはClassのメソッドだが、特定のインスタンスにしか適用されず、これは新しい(名前を持った)クラスメソッドを生成することと等価だ。「クラス」という言葉が何度も出てきて紛らわしく聞こえるので、ここで例を見てみよう。


book.class.instance_eval do
  def content_description
    "Book class"
  end
end

Book.content_description # => "Book class"
book.content_description # => undefined method `content_description'

 ここでは、instance_evalをBookクラスで呼び出し、その結果Bookのクラスメソッドが作られたが、これはbookというインスタンスからは呼び出せない。

 class_evalメソッドの背後にある考え方は、instance_evalの背後にある考え方に似ているが、class_evalはClassにのみ実装されており、Objectには実装されていない。class_evalのコンテキストでメソッドを実行すると、それに対応するクラスメソッドが実行される。class_evalのコンテキストでメソッドを定義した場合、新しいインスタンスメソッドが生成される。例を挙げてみよう。

book.class.class_eval do
  def location
    "Russia"
  end
end

book.location # => "Russia"

book2 = Book.new('Lord of the Rings', 
                 {:author => 'Tolkien'}, 
                 ['Steve', 'Amanda', 'Marty'])
                
book2.location # => "Russia"

 最初ののclass_evalメソッドの呼び出しによって、Bookクラスに新しいインスタンスメソッドが生成される。従って、bookでもbook2でも場所を尋ねることができる(この実装では、答は少し単純すぎるが)。また、instance_evalメソッドを使って以前このクラスに対して定義した、content_descriptionクラスメソッドも呼び出すことができる。次のような形だ。

book.class.instance_eval do
  def content_description
    "Book class"
  end
end

book.class.class_eval "content_description" # => "Book class"
book.class.instance_eval "content_description" # => "Book class"

 最後に、これらをすべて1つにして、図書館とその図書館の中の本を作るDSLを作成しよう。以下に、そのDSLの使用例を挙げる。

library = Library.new do

  new_book :title => 'War and Peace', 
                    :author => 'Tolstoy', 
                    :isbn => '0375760644', 
                    :owners => ['Amanda']

  new_book :title => 'Lord of the Rings', 
                    :author => 'Tolkien', 
                    :owners => ['Steve', 'Amanda', 'Marty']

end

 さらに、以下のコードを実行すると、図書館のインスタンスの中身が作られる。


#,
   #]>

 では、このDSLの実装はどのようなものになるだろうか。明らかに、本を生成する手段が必要なので、これまでの実装の1つを使って作ってみよう(どの実装を使うかはあまり重要ではない)。


class Book

def initialize(title, details = {}, owners = [])
@title = title
@author = details[:author] || 'Unknown'
@isbn = details[:isbn]
@owners = owners
end

end

 次に、図書館のクラスを書く必要がある。図書館を作るときにはコードのブロックを実行したいので、initializeメソッドにオプションブロックを渡す必要がある。このブロックの中で、new_bookと呼ばれるメソッドへアクセスしたいのだが、DSLをきれいにしておくために、ブロックのレシーバーを指定しないようにしたい。このため、レシーバーは暗にライブラリにしたい。これを実現するため、instance_evalメソッドを使ってブロックを評価するようにした。その結果、この例のコードは次のようになった。

class Library
  
  def initialize(&block)
    @books = []
    self.instance_eval(&block)
  end
  
  def new_book(parameters)
    title = parameters.delete(:title)
    owners = parameters.delete(:owners)
    @books << Book.new(title, parameters, owners)
  end
  
end

 ここで重要なことは実装の細部ではなく、特定の分野の詳細を明確に表現できる言語を作るのに必要な機能と柔軟性をRubyが持っており、多くのプログラミング言語にありがちな多くのがらくたや容れ物とは無縁だということだ。これらの例が、読者に自分のDSLを作ってみようという気にさせ、今使っているプログラミング言語で直面している問題を超えられるRubyの機能をさらに学んでみようという気にさせられればよいと願っている。

この記事は海外CNET Networks発のニュースをシーネットネットワークスジャパン編集部が日本向けに編集したものです。海外CNET Networksの記事へ

ブログの新規登録は、2021年12月22日に終了いたしました。

folllow builer on twitter
このサイトでは、利用状況の把握や広告配信などのために、Cookieなどを使用してアクセスデータを取得・利用しています。 これ以降ページを遷移した場合、Cookieなどの設定や使用に同意したことになります。
Cookieなどの設定や使用の詳細、オプトアウトについては詳細をご覧ください。
[ 閉じる ]