最近、友達と毎週メタプログラミングRubyの読書会をやっています。
というのも、友達が転職して🎉、業務でRubyを使い始めることになり、自分も業務で少し使っているので、お互い知識を共有しつつ学びがあれば良いなと思って始めました。
序盤の4章ぐらいまでは特に難しいとは思わなかったんですが、define_methodやevalなどを使って動的にメソッドを定義したり、クラスマクロを作ったりするところはやはりメタプロ脳が足りないのか、少し難しく感じました。
普段、JavaやScalaを使っているので、ここまで自由に書けるのも面白いけど、業務のコードでメタプログラミングしまくると保守が大変そう...というイメージ。
でも、ライブラリに手を入れたり、少しだけここのコードを書き換えたいみたいなときは便利なんだろうな。
まさに、「大いなる力には大いなる責任が伴う」ですね。
内容について
手書きの図を使って、継承チェーンやメソッド探索の動きを説明してくれていたり、出てくるキーワードは理解しやすい言葉で説明されています。
ところどころクイズもあって、解きながら読み進めると理解も深まるし、本としてはおもしろく読めると思います。
Rubyの言語仕様をざっくり理解したあとに読むと良いんじゃないかな。
ノート
2章 月曜日: オブジェクトモデル
- 共通のクラスを持つオブジェクトは、メソッドも共通している。つまり、メソッドはオブジェクトではなく、クラスに存在する
- レシーバー
- 継承チェーン Class -> Module -> Object -> Kernel -> BasicObject
- ancestors メソッド
- include, extend, prepend
- included, extended, prepended
- Object クラスが Kernelモジュールをインクルードしているので、すべてのオブジェクトの継承チェーンにKernelモジュールが挿入されている
- https://github.com/awesome-print/awesome_print
- self
- クラスやモジュールの定義の内側(メソッドの外側)では、selfの役割はクラスやモジュールそのものになる。
- Refinements
- モジュール を書いてから、モジュール定義のなかで refine を呼び出す
- 変更を反映にするには、using メソッドを使って明示的に有効にする必要がある。
- Refinements が有効になるのは 2 箇所だけ
- refine ブロックそのものと
- using を呼び出し た場所からモジュールの終わりまで(モジュール定義にいる場合)またはファイルの終わりまで(トップレベルにいる場合)た
- Kernel.private_instance_methods.grep(/^print/) // => [:printf, :print]
3章 火曜日:メソッド
- メソッドを呼び出すということは、オブジェクトにメッセージを送っていること。
- sendメソッド
- どんなメソッドも呼び出せて、privateでさえも呼び出せる
- define_method
- 動的にメソッドを定義できる
- data_source.methods.grep(/^get(.*)info$/) { Computer.define_component $1 }
- メソッド呼び出しを method_missing に集中させ、ラップしたオブジェクトに転送する
- respond_to_missing?
- respond_to? は、respond_to_missing? というメソッドを呼び出している
- const_missing
- ゴーストメソッドの名前と、継承した本物のメソッドの名前が衝突すると、後者が勝ってしまう
- こうした最小限のメソッドし かない状態のクラスをブランクスレートと呼ぶ。Ruby には、最初からこのブランクスレートが用意されている。
- undef_method
- メソッド呼び出しへのレスポンスを止める
- remove_method
- 現在のクラスからメソッドを削除する
- 継承元にある場合は呼ばれる
- 現在のクラスからメソッドを削除する
4章 水曜日: ブロック
- ブロックはメソッドに渡され、メソッドは yield キーワードを使ってブロックをコールバックする。
- メソッドの内部では、Kernel#block_given? メソッドを使ってブロックの有無を確認できる。
- スコープゲート
- def, class, module
- Class => Class.new
- 変数を共有できるように => フラットスコープ
- define_method
def define_methods shared = 0 Kernel.send :define_method, :counter do shared end Kernel.send :define_method, :inc do |x| shared += x end end define_methods counter # => 0 inc(4) counter # => 4
- instance_eval
- instance_eval に渡したブロックは、レシーバを self にしてから評価されるので、レシーバの private メソッドや @v などのインスタンス変数にもアクセスできる。また、他のブロックと同じよ うに、instance_eval を定義したときの束縛も見える。
- ブロックを評価するためだけにオブジェクトを生成することもある。このようなオブジェクトは、クリーンルームと呼ばれる。
- クリーンルームにはメソッドもインスタンス変数もあまり増やさないほうがいい。ブロックに伴う環境のメソッドやインスタンス変数と名前が衝突する可能性があるからだ。クリーンルームとして使うには、BasicObject のインスタンスが最適である。
- コードを保管できるところは少なくとも3つある
- Proc <- ブロックオブジェクト
- lambda <- Procの変形
- メソッドのなか
Procオブジェクト
inc = Proc.new {|x| x + 1} inc.call(2) この技法は、あとで評価と呼ばれるもの。
- ブロックをProcに変換する2つのカーネルメソッド
- lambda
- proc
dec = lambda {|x| x - 1} dec.class # => Proc dec.call(2) # => 1
p = ->(x){ x - 1} # => lambda {|x| x - 1} と同じ 「矢印ラムダ」演算子で lambda を生成する方法
- yieldでは足りないケース
- 他のメソッドにブロックを渡したいとき
- ブロックをProcに変換したいとき
&
をつけると「メソッドに渡されたブロックを受け取って、それをProcに変換したい」という意味になる&
をつけなければ、Prcoのままになる
- Proc をブロックに戻したいときも
&
を使う - Proc と lambda には 2 つの違いがある。
- ひとつは、return キーワードに関すること
- もうひとつは、引数のチェックに関すること
return キーワードの違い
def double(callable_object) callable_object.call * 2 end l = lambda { return 10 } double(l) # => 20
def another_double p = Proc.new { return 10 } result = p.call return result * 2 # ここまで来ない! end another_double # => 10
引数のチェックの違い
- 一般的に lambda のほうが Proc(や普通のブロック)よりも引数の扱いに厳しい。違った項数で lambda を呼び出すと、ArgumentError になる。Procは引数列を期待に合わせてくれる。
p = Proc.new {|a, b| [a, b]} p.call(1, 2, 3) # => [1, 2] p.call(1) # => [1, nil]
UnboundMethod
- UnboundMethod は、元のクラスやモジュールから引き離されたメソッドのようなものである。Method を UnboundMethod に変換するには、Method#unbind を呼び出す。
5章 木曜日: クラス定義
- 特異クラス(別名: シングルトンクラス)
- Rubyのプログラムは、常にカレントオブジェクトselfを持っている。それと同様に、常にカレントクラス(あるいはカレントモジュール)を持っている
- カレントクラスを変更するには、classキーワード以外の方法が必要
Module#class_eval
メソッドModule#class_eval
はBasicObject#instance_eval
とはまったく別物- instance_eval はselfを変更するだけだが、class_evalはselfとカレントクラスを変更する
- クラスインスタンス変数
- アクセスできるのはクラスだけであり、クラスのインスタンスやサブクラスからはアクセスできない
class MyClass @my_var end
- クラス変数
- サブクラスや通常のインスタンスメソッドからもアクセスできる
class Loan def initialize(book) @book = book @time = Loan.time_class.now end def self.time_class @time || Time end def to_s ... end end
特異メソッド
- 単一のオブジェクトに特化したメソッドのことを
特異メソッド
という - クラスメソッドはクラスの特異メソッド
def object.method # メソッドの中身 end
上記のobjectの部分は、オブジェクトの参照、クラス名の定数、selfのいずれかが使える
クラスマクロ
- attr_*族メソッドはModuleクラスで定義されているので、selfがモジュールであっても、クラスであっても使える。
- attr_accessorのようなメソッドはクラスマクロと呼ぶ
class Book def title #... def subtitle #... def lend_to(user) puts "Lending to #{user}" # ... end def self.deprecate(old_method, new_method) define_method(old_method) do |*args, &block| warn "Warning: #{old_method}() is deprecated. Use #{new_method}()." send(new_method, *args, &block) end end deprecate :GetTitle, :title deprecate :LEND_TO_USER, :lend_to deprecate :title2, :subtitle
特異クラス
- あなたが見ているクラスとは別に、オブジェクトは裏に特別なクラスを持っている
- それが特異クラスと呼ばれるもの
class << an_object # << あなたのコードをここに end 特異クラスの参照を取得したければ、スコープの外にselfを返せばよい obj = Object.new singleton_class = class << obj self end singleton_class.class # => Class "abc".singleton_class # => #<Class::#<String:0x331df0>
- インスタンスを一つしか持てない(だから、シングルトンクラスと呼ばれる)
- 継承ができない
- オブジェクトが特異メソッドを持っていれば、特異クラスのメソッドから探索をはじめる
Rubyオブジェクトモデルの7つのルール
- オブジェクトは 1 種類しかない。それが通常のオブジェクトかモジュールになる。
- モジュールは 1 種類しかない。それが通常のモジュール、クラス、特異クラスのいずれかになる。
- メソッドは 1 種類しかない。メソッドはモジュール(大半はクラス)に住んでいる。
- すべてのオブジェクトは(クラスも含めて)「本物のクラス」を持っている。それが通常のクラスか特異クラスである。
- すべてのクラスは(BasicObject を除いて)ひとつの祖先(スーパークラスかモジュール)を持っている。つまり、あらゆるクラスが BasicObject に向かって 1 本の継承チェーンを持っている。
- オブジェクトの特異クラスのスーパークラスは、オブジェクトのクラスである。クラスの特異クラスのスーパークラスはクラスのスーパークラスの特異クラスである(3回唱えてみよう。もっと早く。図 5-5「特異クラスと継承」を見返せば、納得できるはずだ)。
- メソッドを呼び出すときは、Ruby はレシーバの本物のクラスに向かって「右へ」進み、継承チェーンを「上へ」進む。Ruby のメソッド探索について知るべきことは以上だ。
クラス拡張
- Object#extend
- レシーバの特異クラスにモジュールをインクルードするためのショートカット
module MyModule def my_method; 'hello'; end end obj = Object.new obj.extend MyModule obj.my_method # => 'hello' class MyClass entend MyModule end MyClass.my_method # => 'hello'
メソッドラッパー
- エイリアス
- alias_method :m, :my_method
- 新しいメソッド名を先に、古いメソッドをあとに書く
- メソッドの再定義
- 元のメソッドを変更することではない。新しいメソッドを定義して、元のメソッドの名前をつけること
class Integer alias_method :old_plus, :+ def +(value) self.old_plus(value).old_plus(1) end end
第6章 金曜日: コードを記述するコード
- instance_eval
- class_eval
- Kernel#eval
- Bindingオブジェクト
- Bindingオブジェクトにはスコープは含まれているが、コードは含まれていないため、ブロックよりも「純粋」なクロージャーと考えることができる
- 取得したスコープで評価するには、evalの引数にBindingを渡せばいい
class MyClass def my_method @x = 1 binding end end b = MyClass.new.my_method eval "@x", b # => 1
- 定数: TOPLEVEL_BINDING
- トップレベルのスコープのBinding
- これを使えば、トップレベルのスコープにプログラムのどこからでもアクセスできる
class AnotherClass def my_method eval "self", TOPLEVEL_BINDING end end AnothierClass.new.my_method ## => main
- gem: Pry
- Bindingをうまく活用している
- binding.pryを呼び出すと、現在のbindingでRubyのインタプリタが開かれる
- つまり、実行中のプロセスの中
- instance_eval, class_evalはコード文字列またはブロックのいずれかを
- evalでコードインジェクションに気をつける
- 自分で書いた文字列のみevalを使うように制限すれば良い
- オブジェクトが汚染されているか
- tainted? を使う
- Rubyにはファイル名を受け取り、そのファイルのコードを実行するメソッドが用意されている
- Kernel#load
- Kernel#require
クイズ: アトリビュートのチェック(手順2)
- クラスメソッドを定義するには、クラスのスコープに入る必要がある
- クラスのスコープに入るには、class_evalを使えば良い
クイズ: アトリビュートのチェック(手順4)
- attr_checkedをあらゆるクラス定義で使うには、Class, またはModuleのインスタンスメソッドにすればよい
- self.inherited
- include
- prepended
- Module#extendedをオーバーライドすれば、モジュールがオブジェクトを拡張したときにコードを実行できる
- 以下のフックは、オブジェクトのクラスに住むインスタンスメソッドにしか使えない
- Module#method_added
- Module#method_removed
- Module#method_undefined
- 特異メソッドのイベントをキャッチするには以下を使う
- Kernel#singleton_method_added
- Kernel#singleton_method_removed
- Kernel#singleton_method_undefined
- 以下のフックは、オブジェクトのクラスに住むインスタンスメソッドにしか使えない
クイズ: アトリビュートのチェック(手順5)
module CheckedAttributes def self.included(base) base.extend ClassMethods end module ClassMethods def attr_checked(attribute, &validation) define_method "#{attribute}=" do |value| raise 'Invalid attribute' unless validation.call(value) instance_varialbe_set("@#{attribute}", value) end define_method attribute do instance_variable_get "@#{attribute}" end end end end