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が代入されることで
私達の期待通りの挙動を示します。