Ruby
メタプログラミング
新人プログラマ応援
Rubyist
黒魔術入門

[随時更新]Ruby中級プログラマの為のメタプログラミングまとめ

概要

メタプログラミングRuby(以下メタプロと略す)を読んでいます。

EffectiveRubyも勿論おすすめですが、

どちらかというとメタプロの方が万人にオススメできる内容。

記事はかなり長くなります。

EffectiveRubyは業務で既にRubyを使ってる人向きでした。

メタプログラミングというと、

  • 言語仕様をゴリゴリ書き換える黒魔術
  • 読みたくない他人のコードスタイル第一位

という印象がありますが、

それよりもRubyにおける抽象概念を使ったプログラミングというイメージを持ったほうがいいと思いました。

他にもメタプロの記事は多数ありましたが、
更新されていなかったり、
個人メモでほぼコードが載っていなかったりしたので、

これを機にQiita一詳しいメタプログラミングRuby書籍解説記事を目指して頑張ろうと思います。

対象読者

  • Ruby入門書はクリアしたぜ、でもオブジェクトモデルとかrefinementsってなぁに?
  • メタプログラミングRuby持ってるぜ!でもよくわかんないぜ…

留意点

この記事はあくまで

こういう事理解してるとRubyについて詳しいんじゃないの!?

という趣旨で書かれています。

この記事あるから本書は読まなくていいよ!という内容では無いです。

それなり詳しく書かれた本書の目次だと思って下さい。

本書の内容を越える事は未来永劫ありません。

後初心者向けの記事ではないのでそれなり理解してる前提で進めます。

また、irbなどのインタプリタを起動する際には、

サンプルとしてのコードに関係する部分のみ抽出しています。

返り値などがいちいち表示されてしまうと混乱を招くので。


インストロペクション

Rubyの世界に存在する様々な言語要素。

クラス、メソッドに変数。

それら言語要素に近づいて質問することをインストロペクションと言います。

具体的には、

irb(main)001:0> "String".class
=> String
irb(main)002:0> [1,3,5,7].class
=> Array

このように、メソッドを介して言語要素に質問します。

本書にはActiveRecordの実例を用いて

メタプログラミングの魅力について簡潔かつ効果的に説明していますが、

前提条件として

  • RDBをマッピングしたクラス
  • インスタンスはテーブルの1レコードに対応している

ようなコードを作っていて、そのコードがそれなりに多いので、
本記事では割愛します。

オブジェクトモデル

本書には

Rubyにおける全ての言語要素は、

オブジェクトモデルというシステムの中に共存しています。

といった内容の記述がありますが、

Rubyでは(メソッドを除く)すべてのものはオブジェクトであるという特徴を

より深く理解する為の章です。

例えば、

object_model.rb
class C < Array ; end

obj = C.new
obj.class         #=> C
C.superclass      #=> Array
Array.superclass  #=> Object

というのは私達にとって親しみのあるコードですが、

irb(main)001:0>Array.class
=> Class
irb(main)002:0>Class.class
=> Class
irb(main)003:0>Class.superclsas
=> Module
irb(main)004:0>Object.class
=> class

というような結果は全く未知の存在ではないかと。

これらRubyにおけるオブジェクトの性格を勉強していきましょう。

オープンクラス

まず、Rubyのクラス定義における新しい知見を紹介しましょう。

3.times do
    class Sample
        puts "Hello"
    end
end

入門書の内容と比べると中々違和感のあるコードですよね。

しかしこのコードは問題なく作用しますし、

Helloと3回出力されます。

クラスと聞くと特別な構文というイメージがありますが、

Rubyにおいてはスコープ演算子のような振る舞いをします(だからといってスコープの用途で使ってはいけません。)

ここで1つ疑問が生まれます。

このコードは3回クラスを定義しているのか…?

答えは「NO」です。

それを示すコードを御覧ください。

メタプログラミング的に言えばかなり冗長かもいしれませんが。

class C
  def greeting
    puts "hello"
  end
end

C.new.greeting       #=> hello

class C
  def greeting
    puts "hey"
  end
end

C.new.greeting        #=> hey

先程の 3.times doを分割したような感じ。
このコードからわかるように、

  1. クラスを定義する。
  2. インスタンスメソッド呼び出しでhelloが出力される
  3. 既成のクラスをオープンしてメソッドを書き換える
  4. 書き換えられた文字列が出力される

事が確認出来ると思います。

この技法を "オープンクラス" といいます。

この技法が特に活かされるような場面とはなんなのでしょうか?

例えば、得点を自動で計算するツールがあるとしましょう。

このツールでは、

  • 配列に格納された各点数を合計する
  • 末尾に"点"をつける
  • 合計が100点以上なら追加で文字列が出力される

ような機能があるとしましょう。

まず考えられるコードは、クラスを作る事です。

sample.rb
class ScoreSumFormat
  def initialize(scores)
    result = scores.sum
    out(result)
  end

  def out(before)
    after = before.to_s+"点"
    puts after
    puts "congratulations!" if before >= 100
  end
end

irb(main)001:0>require(sample.rb)
irb(main)002:0>ScoreSumFormat.new([10,30,50,70,90])
>250
congratulations!
irb(main)003:0>ScoreSumFormat.new([10,30])
>40

これで要件は達成されていますが、このコードにはいくつかの弊害があります。

  • 得点を調べる際に常にインスタンス生成が必要
  • インスタンス生成時にいちいち引数を渡す必要がある

このツールは全ての配列に使うような機能ですから、

次のコードの方が望ましいです。

class Array
    def scoreformat
        result = self.sum
        puts "#{result.to_s}点"
        puts "congratulations!" if result >= 100
    end
end

[10,30,50,70,90].scoreformat    #=>  250点, congratulations!

コードもスッキリして可読性も高まり、

機能の意味合いとしても正しくなりました。

そんなオープンクラスですが、これには留意点があります。

それは、既存のライブラリに変更を加える事は、その影響範囲も大きいということです。

ご存知だとは思いますが、

全ての配列はArrayクラスのインスタンスです。

irb(main)001:0>[1,3,5].class
>Array
irb(main)002:0>[[1,3],[2,4],[10,20]].each |ary|
irb(main)003:1*p ary.class
irb(main)004:1>end
>Array
>Array
>Array

Arrayクラスに変更を加えるということは、

変更した時点からソース内の全ての配列に影響が及びます。

言ってしまえばグローバル変数のような危険性を孕みます。

組み込みライブラリに変更を加える危険性の実例を紹介します。


先程のツールですが、

  • 合計を、見やすい文字列にフォーマットする

部分に注目して、

メソッド名をappend(点や御祝い文を追加している)にしてみました。

sample.rb
class Array
    def append
        result = self.sum
        puts "#{result.to_s}点"
        puts "congratulations!" if result >= 100
    end
end 

ここで 「あれ?」 と思った方は鋭い。

irbで挙動を確認してみましょう。

irb(main)001:0>[1,3,5].append(7)
>[1,3,5,7]
irb(main)002:0>require "sample.rb"
irb(main)003:0>[1,3,5].append(7)

ArgumentError (wrong number of arguments (given 1, expected 0))

クイズ

実行結果を見れば簡単に理解できるかと思いますが、

何が起こったか説明できますか?


正解は、

Arrayクラスに存在していたappendメソッドが書き換えられてしまった。

ですね。

このように、安直な名前付けや、気軽にBuilt-in-libraryを書き換える事を侮蔑するスラングにモンキーパッチという言葉があります。

この言葉には二つの意味があって、

前述したように悪いコードに対して使う意味もあれば、

意図的で計算されたオープンクラスの意味もあります。

appendメソッドの例は確実に悪いコードですが

(比較的大規模なツールであれば、書き換える前のappendメソッドを使っているコードがどこかにあるかもしれない為)、

正しいパッチがコードにいい影響を及ぼす事がわかったと思います。


オブジェクトモデル詳細

ここから、Rubyのオブジェクトを本格的に理解していきます。

まずは、インスタンスの中身です。

class Klass
  def foo(name)
    @name = name
  end
end

obj = Klass.new
obj.foo("Bob")
p obj                   #=>  #<Klass:0x0000000003817718 @name="Bob">

オブジェクトIDの値は実行環境によって異なります。

ここでまずわかるのは、

  • インスタンス変数はインスタンスに含まれていること

です。
この事は、次のようなコードからもよくわかります。

class Klass
  def foo(name)
    @name = name
  end
end

obj = Klass.new
obj.foo("Bob")
p obj              #=>#<Klass:0x0000000003817718 @name="Bob">
obj2 = Klass.new
p obj2             #=>#<Klass:0x0000000003a13ee0>

これらのコードから、

  • インスタンス変数は代入されて初めて存在する

ということが明確に把握できました。

続いて、メソッドについてです。

irb(main)001:0>class Klass
irb(main)002:0>end
irb(main)003:0>obj = Klass.new
irb(main)004:0>obj.methods
[:instance_variable_set, :instance_variable_defined?, :remove_instance_variable, :instance_of?, :kind_of?, :is_a?, :tap, :instance_variable_get, :singleton_method, :method, :public_method, :define_singleton_method, :public_send, :extend, :pp, :to_enum, :enum_for, :<=>, :===, :=~, :!~, :eql?, :respond_to?, :freeze, :inspect, :object_id, :send, :to_s, :display, :nil?, :hash, :class, :singleton_class, :clone, :dup, :itself, :yield_self, :taint, :tainted?, :untaint, :untrust, :untrusted?, :trust, :frozen?, :methods, :singleton_methods, :protected_methods, :private_methods, :public_methods, :instance_variables, :!, :equal?, :instance_eval, :==, :instance_exec, :!=, :__id__, :__send__]

大量のメソッドが表示されますが、細かくは見なくて大丈夫です。

ここで、それぞれをメソッドをぼんやり眺めてみると、

これらメソッドが

生成したクラスの親クラスのインスタンスメソッドで有ることに気づきます。

え?「親クラスって、作ったクラスは継承なんてしていないぞ!」

確かにそのとおりですね。おっしゃる通り明示的には継承していません。

しかし、

irb(main)001:0>class Klass; end
irb(main)002:0>Klass.ancestors
=>[Klass, Object, Kernel, BasicObject]

ancestorsメソッドは、その名の通りクラスの祖先を返します。

具体的には、クラスの継承階層を一番上まで登ります。

(Kernelモジュールについては別途説明が必要なので後々。)

クラスは定義されると、基本的にObjectクラスを継承します。

つまり、先程の
irb(main)004:0>obj.methods

が返した多くのメソッドは、

継承階層の上層部のメソッドが表示されている事がわかりますね。

これらメソッドは、オブジェクトではなくクラスが持っています。

今回はオブジェクトに対するメソッドなので、インスタンスメソッドですね。


最後に、クラスについてです。

冒頭の記述を覚えていますか?

Rubyでは(メソッドを除く)すべてのものはオブジェクトである

ということです。

したがって、クラスもオブジェクトです。

具体的には、

Classクラスのインスタンスです。

irb(main)001:0>class C; end
irb(main)002:0>C.class
=>Class
irb(main)003:0>Class.ancestors
=> [Class, Module, Object, Kernel, BasicObject]

Classクラスは、Objectクラスを継承しています。
Objectクラスには全てのオブジェクトに共通したメソッドなどが定義されています。

ところで、このModuleKernelはなんなのでしょうね?

まずは、それぞれが何のクラスのインスタンスなのか聞いてみたいと思います。

irb(main)001:0>Module.class
=>Class
irb(main)002:0>Kernel.class
=>Module
irb(main)003:0>module M; end
irb(main)004:0>M.class
=>Module

まず、こちら側で定義したMモジュールとKernelモジュールが同じ挙動を示していることから、
Kernelモジュールが一般的に定義されたものだとわかります。
しかし、Module定数はクラスでした。

irb(main)001:0>Module.instance_methods(false)
=> Module.instance_methods(false)
=> [:<=>, :<=, :>=, :==, :===, :included_modules, :include?, :name, :ancestors, :attr, :attr_reader, :attr_writer, :attr_accessor, :instance_methods, :public_instance_methods, :protected_instance_methods, :private_instance_methods, :constants, :const_get, :const_set, :const_defined?, :class_variables, :remove_class_variable, :class_variable_get, :class_variable_set, :class_variable_defined?, :public_constant, :freeze, :inspect, :deprecate_constant, :private_constant, :const_missing, :singleton_class?, :prepend, :class_exec, :module_eval, :class_eval, :include, :<, :>, :remove_method, :undef_method, :alias_method, :protected_method_defined?, :module_exec, :method_defined?, :public_method_defined?, :to_s, :instance_method, :public_class_method, :private_method_defined?, :private_class_method, :public_instance_method, :define_method, :autoload, :autoload?]

irb(main)002:0>Class.instance_methods(false)
=>[:new, :allocate, :superclass]

attr_accessorメソッドやinclude?など、
私達にとって馴染みのあるインスタンスメソッドを持っていることがわかります。

ここで、全てのクラスがアトリビュートを定義できるのは継承階層の上位で定義されていたから(しかもレシーバは明示しなくていい)事がわかります。

モジュールは、(new,allocate,superclassというメソッドを持っていない点を除いて)クラスと似た性質を持っています。

インスタンスの生成や継承は出来ませんが。

これでModuleクラスについてはわかりましたね。

  • Moduleクラスに定義されたインスタンスメソッドはモジュールとクラスの両方で使える。
  • Classクラスで定義された3つのインスタンスメソッドを使うことでクラスはインスタンスを生成出来る。

次にKernelモジュールの解説に移ります。

irb(main)001:0>Kernel.private_instance_methods(false)
=>[(中略),:Array,:print,(中略)]

どうやらKernelモジュールには、私達が頻繁に用いるメソッドが定義されているようです。
これらもattr_accessorメソッドなどと同じく、関数型メソッド呼び出しのように使えます。

これも、全てのクラスがKernelモジュールを特異クラスとして継承しているからですね。


コラム:mainオブジェクト

あ、因みに、Rubyソースコードのトップレベルは暗黙的にmainオブジェクトの中に入っています。
その証拠に、

p self       #=>main
self.class   #=>Object

と表示されます。
selfについてよくわからない人も後でちゃんと説明するので安心してくださいね。
このmainオブジェクトのスコープ内=プログラムに記述したプレーンなトップレベルは、
ObjectクラスがKernelモジュールをincludeしてるので
(もしかしたらBasicObjectprependしてる可能性もある…
prependメソッドについては後述します!)

私達が安心してソースコードの一行目に

print "トップレベルだああああああああああああああああああああああ!!!!\n"

と記述できるのはこれらの力が働いてくれるからなんですね。


「モジュールを継承なんてしてねえぞ!」

もしかして、この記事見てないんですか?(宣伝すみません)

Rubyの継承階層におけるmoduleの位置関係を解説する。@Effective Ruby

こちらの記事をご一読頂ければ理解できるかと。

因みに、インクルードしたクラスのに特異クラスを挿入するのがinclude
に挿入するのがprependです。


定数についての話がその後述べられていますが、
Rubyの定数とフリーズについて
Rubyの名前空間とレキシカルスコープについて

という記事で説明しているのでそちらをご参照下さい。

メソッドについて

先程までの復習をすると、

  • メソッドはオブジェクトには存在せず、そのオブジェクトが属するクラスに定義されている。
  • スーパークラスから継承されたメソッドは(クラス内であれば)関数的メソッド呼び出しのように使える

ということでしたね。

ここで、次のようなコードについて見てみます。

sample.rb
class Parent
    def foo(key="Ruby")
        puts "#{key} is fun!"
    end
end

class Child < Parent

end

Child.new.foo()                     #=>  Ruby is fun!

至って普通のコードですよね。
しかしこれらコードの裏でRubyがやっていることを知れば、
クラス定義の際により効果的なインスタンスメソッドを定義できるかもしれません。


冒頭に「後初心者向けの記事ではないのでそれなり理解してる前提で進めます。
と書きましたが、敢えて継承の説明も交えながら話します。
例えば、先程の例で考えてみましょう。

foo.rb
class C
    def foo()
        puts "foo method was called"
    end
end

C.ancestors                       #=>[:C,:Object,:Kernel,:BasicObject]
C.new.foo()                       #=>foo method was called

クラスを定義して、インスタンスメソッドでputsを呼び出す簡単なコードです。

irbで確認したとおり、
putsメソッドは継承階層でも上位のKernelモジュールで定義されています。
逆に言えば、Cクラスにはputsメソッドは定義されていないことになります。

しかし、このコードは元気に動きますし、皆さんもこのコードが動くことを信じて疑いません。

しかしputsというのは単なるメソッドで、Kernelモジュールは単なるモジュール(又は特異クラス)の1つだと説明しました。

つまり普通であればCクラスに代入されていないメソッドを使えるのは変です。

しかし皆様御存知の通り、
サブクラスのインスタンスはスーパークラスに定義されたインスタンスメソッドをよびだせます。

そのおかげで多くのメソッドを使えているわけです。

ではどうやって親クラスのメソッドを見つけている?

という話ですよね。

  1. まず、メソッド呼び出しのレシーバに特異メソッドが定義されていないかを確認します(オブジェクトに対する特異メソッドの例は下に)
  2. レシーバのクラス("レシーバは○○クラスのインスタンス"という言葉の〇〇)に呼び出されたメソッドがあるかどうか調べる。あった場合はそのメソッドを呼び出す。
  3. なければ、継承階層を1つ登る。そのクラスでもなければまた1つ登る。
特異メソッド例.rb
class C
    def foo
        puts "foo in C"
    end
end

obj = C.new
obj.foo()                #=>foo in C

def obj.foo              #objに対する特異メソッド定義
    puts "foo method"
end

obj.foo()                #foo method   (Cのメソッドが書き換わったわけではない事に注意)       

このように、Rubyはメソッドを見つけた時点で探索を中断し、呼び出されたコードに戻ります。

  1. レシーバの属するクラス(クラスとインスタンスの関係)を探索
  2. サブクラスとスーパークラス(クラスとクラスの関係)を探索

というものの説明によくこういった図が用いられます。

無題.png

この図みたいに、1つ右にいってその後上に上がる事から、

one step to the right, then up
ルールとか呼ばれてます。


selfについて

Rubyの入門書をやって引っかかるランキング第一位だと思ってます。

selfを理解するキーワードは「カレントオブジェクト(=現在のオブジェクト)」です。
それでは行きましょう。

まず、selfを使うような代表的なコードを見ていきましょう。

class C
    def initialize
        puts "new method was called"
    end

    def self.foo
        p self
    end
end

C.new         #=> new method was called
C.foo()       #=> C 

まずこれを見た時に考える事は

fooメソッドが"クラスメソッド"というもの
ということでしょうか。

しかし、クラスメソッドを呼び出す際に、C.fooと、レシーバをクラスにするのは何故なのでしょうか…?


まずは愚直にpメソッドに渡してみます。

p self

class C
    p self

    def foo
        @num = 300
        p self
    end
end

obj = C.new
obj.foo()
実行結果
>main
>C
>#<C:0x00000000032e8d50 @num=300>

先程のコラムで説明したとおり、
トップレベルのselfはmainオブジェクトを表します。

次にクラス定義のスコープに入ると、
selfCクラスを表していることがわかります。

この事は、Rubyがクラス定義内に入ったことで、selfにカレントオブジェクトのCクラスが暗黙的に代入された事を示します。

メソッドは呼び出されるまで無視される為(Syntax Error等は勿論別)
fooメソッド内のselfが出力されるのはメソッド呼び出し時ですね。

さて、実行結果の3行目を見てみると、
出力されたオブジェクトはインスタンス変数を持っている事がわかります。

インスタンス変数は、

  • 代入されてはじめて存在する
  • インスタンス変数を保持しているのはインスタンスである

という事から、
インスタンス変数に代入しているメソッド、つまりfooのレシーバである、
objがselfに代入されていることがわかりますね。

これが、selfにカレントオブジェクトが代入される大まかな説明です。

では改めて問題です。

Q.クラスメソッド定義でself.fooとするのは何故ですか?

ここまでの話をきちんと理解していれば簡単ですね。

答えまで隠します

答えは、
クラス定義のスコープになった時、selfはクラス(オブジェクト)を示し、
クラスメソッドのレシーバにはクラスオブジェクトが該当する為。

selfがカレントオブジェクトを暗黙的に代入する特別な変数ということに気がつけば、
私達が今まで使ってきた変数のメリットを活かすことが可能です。

class C
    def self.foo
        puts "foo"
    end
end

class D < C
end

D.foo()            #=>foo

このように、クラスの継承をした場合でも同様です。

Cで定義されたクラスメソッドは、
継承によってDに受け継がれます。
Dをレシーバとして呼び出しても、selfに自動的にDが代入されることで
私達の期待通りの挙動を示します。


Refinements

先程、オープンクラス技法を使うときの注意点を話したのを覚えていますか?

Arrayクラスに変更を加えるということは、

変更した時点からソース内の全ての配列に影響が及びます。
言ってしまえばグローバル変数のような危険性を孕みます。
組み込みライブラリに変更を加える危険性の実例を紹介します。

という話ですね。
この問題を解決する手段の1つが、Refinementsという魔術です。

module UpdateAppend
    refine Array do
        def append
            result = self.sum
            puts "#{result.to_s}点"
            puts "congratulations!" if result >= 100
        end
    end
end 

module ScoreFormat
    using UpdateAppend
    [10,30,50,70,90].append    #=>  250点, congratulations!
end

[10,30,50,70].append(90)           #=>[10,30,50,70,90]

**refine**が付属したブロック内で定義したメソッドは、
モジュール等のスコープ内でusing モジュール名とすることで、
そのスコープ限定でクラスが開かれ、メソッドが更新されます。

ここで注意してほしいのが、Refinementsがあるから変数名、メソッド名を杜撰に定めていいわけではない
ということです。
今回の例で言えば、確実にappendよりscoreformatの方が適した名前付けといえるでしょう。
オープンクラスはその作用が及ぼす影響を鑑みた上で利用するべきなのは言うまでもありません。

Refinementsのとんでも特性をご紹介します。

class C
    def my
        "original my()"
    end

    def ano
        my()
    end
end

module M
    refine C do
        def my
            "refined my()"
        end
    end
end

using M
p C.new.my()                   #=>"refined my()"
p C.new.ano()                  #=>"original my()"
  • Cクラス。内部で文字列を返すmyメソッドmyを呼び出すとanoメソッドを定義している。
  • Mモジュール。Refinementsによってmyメソッドの文字列を更新している。
  • トップレベル。usingによってmyが更新される。

ここまでの理解は出来ると思いますが、
どうやらメソッドを呼び出した時に、私達が期待したものとは違う結果が返されています。

1つ目の出力では、更新された返り値が出力されています。これは問題ないですね。
2つ目の出力では、anoメソッドが更新されたmyメソッドを呼び出しています。
しかし、結果は更新される前の返り値が出力されました。

これら挙動はRubyの技術的正当な理由による実装が引き起こしているのかもしれません。

Refinementsが悪質なモンキーパッチを取り除く有効な手段であることは事実ですが、
その使い方は適宜検討する必要がある事を示唆しています。

このサンプルコードの是非についてはコメント欄をご参照ください。


メソッド

Rubyの内部におけるオブジェクトモデルを理解した所で、
ここからはメソッドに注目して話を進めていきたいと思います。

本書にはコンピュータを形成する各デバイスの情報を管理するDSクラスの悪しき部分を、
段階的な手法でどんどんリファクタリングする、という流れを取っています。

痛快なリファクタリングについては是非本書を購入してご覧頂ければ幸いです。

本記事ではリファクタリングの過程で学習するメタプログラミングの視点からのアプローチ技法について紹介していきます。

動的メソッド

通常、オブジェクトに対してメソッドを呼び出す時は、

レシーバ.メソッド名

のようにします。
具体的には、

[10,30,50].append(70)       #=> [10,30,50,70]

のようにします。

しかし、上のコードは次のように表すことも出来ます。

[10,30,50].send(:append,70) #=>[10,30,50,70]

実行結果が同じな事が確認できました。

しかし、sendを使う方がコード量も多く、手間がかかるように思います。

では、sendを使う利点は何なのでしょうか?

それは、実行時にメソッドを決定できるという点です。

次のコードを見てください。

FUNCTIONS = %w(append delete)

def introduce(ary)
    puts "要素追加: 1 要素削除: 2"
    num = gets.chomp.to_i
    puts "メソッドに渡す引数を入力してください。"
    arg = gets.chomp.to_i

    func = FUNCTIONS[num-1]
    ary.send(func.to_sym,arg)
    puts "#{func} was called"

    p ary
end

introduce([10,30,50])
=>要素追加: 1 要素削除: 2
>1
=>メソッドに渡す引数を入力してください。
>100
=>append was called
=> [10,30,50,100]

簡潔ですがコード書きました。
至って簡単で、

  1. 定数FUNCTIONSにメソッド名が定義されている
  2. 入力を受け取ったnumでメソッドを選択、引数も入力を受け付ける。
  3. 選択されたメソッドのfuncargを渡してsendを呼び出す

という処理内容になっています。


このように、呼び出したいメソッド名を通常の引数として渡す事で、
コードの実行時に呼び出すメソッドが決まる事を動的ディスパッチと言います。
(抽象的な意味ではもう少し違うかもしれませんが。)

動的ディスパッチの効果についてまだ信用出来ませんか?
ではもう少しわかりやすく。

皆さんはご自身の学習時間を管理するプログラムを書くとします。

簡単に考えると、こんな感じです。

def learning_manage(base=[])
    puts "現在の累計学習時間は#{base.sum.to_s}時間です。\n履歴は#{base.to_s}です。\n処理を選択してください。"
    puts "時間追加→1 日付平均→2 時間取り消し"
    judge = gets.chomp.to_i

    case judge
    when 1
        puts "追加する時間量を入力してください。"
        hours = gets.chomp.to_i
        base.append(hours)
    when 2
        average = base.sum / base.length
        puts "日付平均は#{average.to_s}です。"
    when 3
        puts "削除する時間量を入力してください。"
        if base.include?(hours = gets.chomp.to_i)
            base.delete(hours)
        end
    end

    p base
end

とにかく安直なコードです。

  • 機能を追加するたびにcase文のwhen節が増える。✗
  • そもそも量が多い。✗

少なくともこれらの問題を抱えています。
このコードは皆さんの手でリファクタリングしてみてください。

sendを用いれば、条件分岐を使わずともメソッド呼び出しを制御出来ます。

簡単なのは、機能名が要素になっている配列を操作することですかね。
それ以外にもハッシュコンテナを使ったり、ラムダ式で書いたりも出来ます。


動的ディスパッチについて大まかにわかったところで、
今度はメソッド定義も動的に行ってみましょう。

class Klass
    define_method :foo |arg|
        return arg if arg.class == String

        arg ** 2
    end

end

p Klass.new.foo("Bob") #=>"Bob"
p Klass.new.foo(300)   #=>90000

このfooKlassクラスのインスタンスメソッドであり、
コード実行で初めて定義されます。
これを動的メソッドといいます。

メソッドの中身はブロックで渡される為、
上記のようにreturnでブロックから抜け出す制御も正常に動いていることがわかります。

またRubyの性格をよく理解している人ならば、

define_method :foo do |arg|
    #...
end

define_method(:foo) do |arg|
    #...
end

この二つのコードが同じ意味合いを示す事は容易にわかるでしょう。

もうわかりましたね?
動的メソッドは通常の引数に与えられた名前の元に定義されるので、
実行時にメソッドを定義するといって差し支えないのです。

動的ディスパッチの時と同じですね。


method_missing

なんだそりゃ?と思った方もご安心を、どんな方も一度は目にしているはずです。

class C; end

C.new.uhyahyaohyohyo

NoMethodError: undefined method 'uhyahyaohyohyo' for #<オブジェクトID>

このエラー、見ますよね?

このエラーが参照しているメソッドがmethod_missingです。
method_missingは、

one step to the right, then up
ルールに従ってuhyahyaohyohyoメソッドを探索します。

勿論あるわけないので、BasicObjectクラス(抽象的にはルートクラス)まで探索したら、
今度は
method_missingメソッドを同じく
one step to the right, then upルールに則って探索します。

このメソッドはBasicObjectクラスに定義されています。

ここで閃いた方は鋭いです。

つまりこのmethod_missingをオーバーライドすれば、
NoMethodErrorの挙動を変えることすら出来ます。

ほぼ本書からの引用ですが、

class Klass
    def method_missing(method,*args)
        puts "called: #{method}(#{args.join(',')})"
    end
end

Klass.new.ahyahyaohyohyo(30,50,70,90)
=>called: ahyahyaohyohyo(30,50,70,90)

本来、method_missingNoMethodErrorraiseするような実装がされています。

しかし、
one step to the right, then upルール
を利用することで、
method_missingの探索プロセスがBasicObjectに行き着く前に探索を終了させられる

からです。

一見呼び出せている(エラーを発生しない)メソッドが、レシーバの特異クラスや継承チェーンを遡っても存在しないような状態の事を、
ゴーストメソッドといいます。

method_missingの説明に欠かせない動的プロキシですが、
これはかなり解説が難しく、誤解を招いてしまいそうな予感があります。

よってここでは解説を割愛致します。すみません。


respond_to?

method_missingについての概要を理解した私達が
Ruby公式ドキュメントを眺めていると、気になる文章を発見しました。

(method_missingの項目にて)
[注意]このメソッドをoverrideする場合は、
対象のメソッド名に対してObject#respond_to?が真を返すようにしてください。
そのためには、Object#respond_to_missing?も同様にoverrideする必要があります。

そもそも、respond_to?を知らない人はいない…ですよね?

irb(main)001:0>[1,2,3].respond_to?(:concat)
=>true
irb(main)002:0>[9,9,9].respond_to?(:ahyohyo)
=>false

オブジェクトがone st(以下略)ルールに則り引数に渡されたメソッドを探索できればTrueを、
メソッド探索の結果method_missingが見つかればFalseを返すようなメソッドです。

このメソッドはとてもよく使われます。

(ここからrespond_to?respond_to_missing?についての解説がありますが、割愛します。)


ブロック

さて、ブロックの話題に入って行きます。

ブロックと聞いて皆さんが真っ先に思いつく(そして理解できずに震える)のは、
yieldだと思います。

def foo
    puts "foo method was called"
    yield if block_given?
end

foo() do
    puts "block was also called"
end #=>foo method was called block was also called

foo()  #=>foo method was called

正体不明のyieldに怯えるのはもう終わり。
yieldはキーワードだと思ってみましょう。

Rubyにおいて簡潔にブロックの復習をして、さっさと本題に戻るとしましょう。

  • ブロックとは処理のひとかたまり。具体的にはメソッド呼び出しの時に引数と一緒に渡せる。
  • ブロックはスコープを制御する。具体的には、ローカル変数がブロック内外でスコープによって分かれる。
  • ブロック内の処理はyieldキーワードでキャッチ出来る。

上記のコードをもう一度ご覧ください。
ブロックが渡されているかを真偽値で返すメソッドblock_given?によって、
ブロック呼び出しを制御し、
ブロックを渡した場合は内部の処理、
でなければyieldが機能していないことがわかります。

では、本書のコードで完全理解と行きましょう。

def a_method(a,b)
    a + yield(a,b)
end

a_method(1,2){|x,y| (x+y) * 3}

さて、このメソッド呼び出しによって返される値はいくつでしょうか?

もしここで

  • まず|x,y|って何なん?
  • yieldに引数渡してるの意味わからん
    となった方はお手持ちの入門書をもう一度参照してくださいね。
    公式ドキュメントでもよし。

  • 9

  • 10

  • Error発生

正解は、10です。


ブロックというのは、ご存知の通り処理がひとまとまりになったものですが、
ブロックのコードを実行するには、主に

  • 束縛
  • コード

の2項目が集まっていることを知るべきです。
それぞれ具体的には、

束縛…オブジェクトに紐付けられた名前
コード…束縛を使った実際の処理部分

のようになっています。

def my_method
  x = "Goodbye"
  yield("Ruby's")
end

x = 'Hello'
my_method do |y|
  puts "#{x}, #{y} world"      #=>Hello, Ruby's world
end

このコードで言えば、メソッド外で定義されたx = Helloのようなものを
束縛というわけですね。
Goodbyeが出力されず、Helloが出力されたのは、
メソッドにある束縛(x = Goodbye)がブロックから見えないからです。

  • メソッドにある束縛はブロックから見えない。
def just_yield
  yield
end

top = 1

just_yield do
  top += 1
  local = 1
end

p top  #=> 2
p local   #=>NoMethodError

メソッド呼び出しの際に定義したブロックでは、
新しくlocalという束縛を定義しています。
しかし、ブロック外で参照するとErrorを吐いています。
つまりこれは、ブロックが閉じる(end)時に束縛を隠してしまう事に起因します。

  • ブロック内の新しい束縛は、ブロック自身によって包まれてしまう。

ここまでくれば、次のようなコードを読むことが出来ます。

def calculate
  num = 30
  yield if block_given?
  p num ** 2  #=>900
end

num = 50
calculate do
  num = 100
  p num ** 2    #=> 10000
end
p num #=>100

では問題です。
ブロック内のnum = 100をコメントアウトすると結果はどうなりますか?


正解は、2500です。

ここまで見てきた事からわかるように、
まずは

some_method do
  #...
end

という形がメソッド呼び出しであることに慣れなければなりません。
メソッド呼び出しにブロックを定義しているので、
ブロック付きメソッド呼び出しなんて言い方をすることもあります。

そして、次の事がわかりました。

  • ブロックの開始時にそれまで定義されていた束縛を包んで持っていく(参照する)。
  • ブロックの終了時、持っていった束縛は元あった場所に返す(値は変化するが参照できる)。
  • メソッド内でブロックが呼び出されても、メソッド内の束縛は参照できない

ここまでくればブロックの基本は大丈夫でしょう。


スコープ

ここからはスコープです。

スコープとは、
上から一行ずつ実行してきて、今自分が実行している場所で見える範囲です。

わかりにくいですね。

ここはメタプログラミングRuby著者の文言を借りるとしましょう。

自分がRubyの小さなデバッガになったと考えてみよう。
ブレークポイントに当たるまで命令文を次々とジャンプしていく。
さて、一息ついてから、周りを見てみよう。
そこで見える景色が、あなたのスコープだ。

スコープ一面に束縛があるはずだ。足元を見れば、ローカル変数がいくつもある。
顔をあげると、自分はオブジェクトの中に立っている事がわかる。
そこにはメソッドインスタンス変数もある。
ここがカレントオブジェクト、つまりselfだ。
遠くの方には定数のツリーも見える。
目を細めてもっと遠くを見れば、グローバル変数がたくさん見える。

top = 1
class Klass
  intocls = 2
  p local_variables

  def my_method
    intomtd = 3
    p local_variables
  end
  p local_variables
end

obj = Klass.new
obj.my_method
obj.my_method
p local_variables
実行結果
[:intocls]
[:intocls]
[:intomtd]
[:intomtd]
[:top, :obj]

実行結果の一行目はクラス定義内の一番上のlocal_variablesメソッドだ。
二行目はクラス定義の一番後ろにあるlocal_variablesメソッド。
三行目と四行目はmy_method呼び出し時のlocal_variablesメソッドが作用しています。
そして最後に、トップレベルのローカル変数です。

このコードと実行結果を見てRubyがどのようにローカル変数を参照したのか
すぐに分かる人はいないと思います。

重要(且つシンプル)なのは、my_methodの結果です。
私達がこのコードを見た時、
無題.jpg

のように思ったはずです。
要は

  • Klassクラスの中にmy_methodがあり、intomtdが定義されている。

しかし、実行結果を見ると、
my_method内でlocal_variablesメソッドを呼び出した結果、
intomtdのみが参照されている事がわかります。

つまり、みてくれである物理的なコードの配置と、
スコープの概念はイコールではないのです。

これはとても重要な事で、且つ複雑で難しい事です。

そこについての理解を、次に深めることにしましょう。