メタプログラミングRuby第2版 Ⅰ部を読んだ

最近、友達と毎週メタプログラミング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 # => 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_evalBasicObject#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 種類しかない。それが通常のオブジェクトかモジュールになる。
  2. モジュールは 1 種類しかない。それが通常のモジュール、クラス、特異クラスのいずれかになる。
  3. メソッドは 1 種類しかない。メソッドはモジュール(大半はクラス)に住んでいる。
  4. すべてのオブジェクトは(クラスも含めて)「本物のクラス」を持っている。それが通常のクラスか特異クラスである。
  5. すべてのクラスは(BasicObject を除いて)ひとつの祖先(スーパークラスかモジュール)を持っている。つまり、あらゆるクラスが BasicObject に向かって 1 本の継承チェーンを持っている。
  6. オブジェクトの特異クラスのスーパークラスは、オブジェクトのクラスである。クラスの特異クラスのスーパークラスはクラスのスーパークラスの特異クラスである(3回唱えてみよう。もっと早く。図 5-5「特異クラスと継承」を見返せば、納得できるはずだ)。
  7. メソッドを呼び出すときは、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

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