最近、友達と毎週メタプログラミングRubyの読書会をやっています。
というのも、友達が転職して🎉、業務でRubyを使い始めることになり、自分も業務で少し使っているので、お互い知識を共有しつつ学びがあれば良いなと思って始めました。
序盤の4章ぐらいまでは特に難しいとは思わなかったんですが、define_methodやevalなどを使って動的にメソッドを定義したり、クラスマクロを作ったりするところはやはりメタプロ脳が足りないのか、少し難しく感じました。
普段、JavaやScalaを使っているので、ここまで自由に書けるのも面白いけど、業務のコードでメタプログラミングしまくると保守が大変そう...というイメージ。
でも、ライブラリに手を入れたり、少しだけここのコードを書き換えたいみたいなときは便利なんだろうな。
まさに、「大いなる力には大いなる責任が伴う」ですね。
内容について
手書きの図を使って、継承チェーンやメソッド探索の動きを説明してくれていたり、出てくるキーワードは理解しやすい言葉で説明されています。
ところどころクイズもあって、解きながら読み進めると理解も深まるし、本としてはおもしろく読めると思います。
Rubyの言語仕様をざっくり理解したあとに読むと良いんじゃないかな。
www.amazon.co.jp
ノート
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
counter
- 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つのカーネルメソッド
dec = lambda {|x| x - 1}
dec.class
dec.call(2)
p = ->(x){ x - 1}
「矢印ラムダ」演算子で lambda を生成する方法
- yieldでは足りないケース
- 他のメソッドにブロックを渡したいとき
- ブロックをProcに変換したいとき
&
をつけると「メソッドに渡されたブロックを受け取って、それをProcに変換したい」という意味になる
- Proc をブロックに戻したいときも
&
を使う
- Proc と lambda には 2 つの違いがある。
- ひとつは、return キーワードに関すること
- もうひとつは、引数のチェックに関すること
return キーワードの違い
def double(callable_object)
callable_object.call * 2
end
l = lambda { return 10 }
double(l)
def another_double
p = Proc.new { return 10 }
result = p.call
return result * 2
end
another_double
引数のチェックの違い
- 一般的に lambda のほうが Proc(や普通のブロック)よりも引数の扱いに厳しい。違った項数で lambda を呼び出すと、ArgumentError になる。Procは引数列を期待に合わせてくれる。
p = Proc.new {|a, b| [a, b]}
p.call(1, 2, 3)
p.call(1)
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
"abc".singleton_class
- インスタンスを一つしか持てない(だから、シングルトンクラスと呼ばれる)
- 継承ができない
- オブジェクトが特異メソッドを持っていれば、特異クラスのメソッドから探索をはじめる
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
class MyClass
entend MyModule
end
MyClass.my_method
メソッドラッパー
- エイリアス
- 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
- 定数: TOPLEVEL_BINDING
- トップレベルのスコープのBinding
- これを使えば、トップレベルのスコープにプログラムのどこからでもアクセスできる
class AnotherClass
def my_method
eval "self", TOPLEVEL_BINDING
end
end
AnothierClass.new.my_method
- gem: Pry
- Bindingをうまく活用している
- binding.pryを呼び出すと、現在のbindingでRubyのインタプリタが開かれる
- instance_eval, class_evalはコード文字列またはブロックのいずれかを
- evalでコードインジェクションに気をつける
- 自分で書いた文字列のみevalを使うように制限すれば良い
- オブジェクトが汚染されているか
- Rubyにはファイル名を受け取り、そのファイルのコードを実行するメソッドが用意されている
- Kernel#load
- Kernel#require
docs.ruby-lang.org
クイズ: アトリビュートのチェック(手順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