メタプログラミング Ruby を読んだ

感想

書くとき、読むとき、レビューするときに、そのコードの解像度が広がるというか、奥に広がる世界にまで意識が届くようになった。読んでいても知識を押し付けられる感覚がないので、楽しみながら Ruby の奥深さを学ぶことができる。

Ⅰ部

1章 頭文字 M

メタプログラミングとは、コードを記述するコードを記述することである。

C++ のようなコンパイル型の言語では、コンパイルすると変数やメソッドはその実体を失う。コンパイル後にインスタンスメソッドのことをクラスに質問できない。Ruby のようなインタプリタ型の言語では、あらゆる言語要素 (変数、メソッド、クラス等) が実行時にも存在している。irb のような対話的にコードを打ち込めるシェルを使っていると、この辺りは実感としてはある。

Active Record は Movie#titleMovie#director= といったメソッドをこっそり定義している。

class Movie < ActiveRecord::Base
end

Active Record は目に触れる機会の多いメタプログラミングなのかもしれない。attr_reader とかも暗黙的にメソッドを定義しているので身近なメタプログラミングと言えるのだろうか。

2章 月曜日: オブジェクトモデル

オープンクラス

いつでも既存のクラスを再オープンして、その場で修正できる。この技法をオープンクラスと呼ぶ。

オブジェクトの中身

インスタンス変数

class MyClass
  def my_method
    @v = 1
  end
end

obj = MyClass.new # この時点では @v は存在しない
obj.my_method # はじめて @v が存在できる

メソッド

オブジェクトにはメソッドはなく、インスタンス変数とクラスへの参照があるだけだ。

インスタンスそのものにメソッドが定義されているわけではない。ただ、「クラス MyClass が my_method を持つ」というのも誤解がある。この明確な呼び分けとして「インスタンスメソッド」と「クラスメソッド」がある。MyClass#my_methodMyClass.my_method のように表記する。

クラスの真相

Ruby のオブジェクトモデルを学ぶときに最も重要なのは「クラスはオブジェクト」ということだろう。

クラスはオブジェクトであり、クラスにもクラスがある。クラスのクラスは Class である。

String.class
#=> Class

Class クラスのインスタンスにはメソッドがある。

Class.instance_methods(false)
#=> [:allocate, :superclass, :subclasses, :new]

new なんかは分かりやすい。

Array クラスは Object クラスを継承している。つまり「配列はオブジェクトである」と言うことができる。

Array.superclass
#=> Object

Ruby ではあらゆるクラスのスーパークラスは Object になる。Object クラスは to_s のようなあらゆるオブジェクトで便利に使えるメソッドを持っている。

モジュール

Class のスーパークラスは Module だ。

Class.superclass
#=> Module

これはちょっと意外だった。Class はインスタンスを生成して使う、モジュールはインスタンスを生成せずに include して使う、のように使われ方の違いが明確であり、Class は Module であると言われると今一つしっくりこない。

クラスはオブジェクトの生成 new や継承元クラスを確認する superclass などの4つのインスタンスメソッドを追加した「モジュール」だ。[筆者要約]

定数

大文字で始まる参照は、クラス名もモジュール名も含めて、すべて定数だ。

定数と変数の違いはなにか。重要な違いは「スコープ」にある。定数のスコープは独自ルールに基づいている。

モジュールおよびクラスがディレクトリで、定数がファイルだ。

module M
  class C
    X = 'constant'
  end

  C::X
end

M::C::X

Rake の例

module Rake
  class Task
  ...
end

Task のような汎用的な名前の定数が衝突しないよう Rake というモジュールでまとめる。このようなモジュールを「ネームスペース」と呼ぶ。Task の完全な名前は Rake::Task となる。

いろいろまとめると

3章 火曜日: メソッド

  • 静的言語
    • 静的型チェックを持つ
      • すべてのメソッド呼び出しに対して、合致するメソッドをオブジェクトが持っているかどうかをコンパイラがチェックする
      • => コードを実行する前に、コンパイラがミスを指摘してくれる
  • 動的言語
    • 型チェックを持たない
      • メソッドの呼び出しをチェックするようなコンパイラは存在しない
      • => オブジェクトにメソッドが実装されていなくても実行可能

動的メソッド

メソッドを呼び出すというのは、オブジェクトにメッセージを送っていることなんだ。

Object#send をイメージすると分かりやすい。send を使うとメソッド名にシンボルが使える。コード実行時に動的に呼び出すメソッドを決定できる。これを動的ディスパッチと呼ぶ。

define_method を使えば、実行時にメソッド名を決定できる。これを動的メソッドと呼ぶ。

class MyClass
  define_method :my_method do |my_arg|
    my_arg * 3
  end
end

obj = MyClass.new
obj.my_method(2) # => 6

ゴーストメソッド

オブジェクトにメソッドが見つからなければ、元のオブジェクトの method_missing を呼び出す。BasicObject の private インスタンスメソッドにそれはある。

method_missing をオーバーライドすると不明なメッセージを途中でキャッチして振る舞いを変えることができる。

class Lawyer
  def method_missing(method, *args)
    puts "You called: #{method}(#{args.join(', ')})"
    puts "(You also passed it a block)" if block_given?
  end
end

bob = Lawyer.new
bob.talk_simple('a', 'b') do
  # block
end
# =>
You called: talk_simple(a, b)
(You also passed it a block)

この特性をうまく活用して存在しないメソッド呼び出しに「あたかもそのメソッドがあるように」見せる手法をゴーストメソッドと呼ぶ。

動的メソッド vs ゴーストメソッド

可能であれば動的メソッドを使い、仕方なければゴーストメソッドを使う

ゴーストメソッドにはバグが生まれやすい。

4章 水曜日: ブロック

ブロックがスコープを制御するのに強力なツールだってことは、まだ知らないんじゃないかな?スコープというのは、変数やメソッドがどのコード行まで見えるかというものだ。

ブロックの基本

ブロックを定義できるのはメソッドを呼び出すときだけ。メソッドに渡されたブロックは yield を使ってコールバックされる。

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

a_method(1, 2) {|x, y| (x + y) * 3} # => 10

例外が発生しても実行しなければいけない処理をシンプルに書くこともできる。

module Kernel
  def with(resource)
    begin
      yield
    ensure
      resource.dispose
    end
  end
end

# 呼び出し側
r = Resource.new
with(r) do
  # 何かしらの処理
end

ブロックはクロージャ

束縛

ブロックは「コード自体」と「束縛の集まり」の2つから構成される。ローカル変数、インスタンス変数、self といったものが束縛される。ブロックを定義した時点でそこにある束縛を取得し、メソッドに束縛ごと一緒に渡す。

def my_method
  x = "Goodbye"
  yield("cruel")
end

x = "Hello"
my_method {|y| "#{x}, #{y} world" } # => "Hello, cruel world"

x は「ブロックを定義したとき」に束縛される。ブロックからメソッドのローカル変数である x は見えない。

スコープ

local_variables を使ってローカル変数を確認することで、スコープの遷移を追跡できる。このコードでは、「トップレベルのスコープ」「MyClass のスコープ」「my_method のスコープ」の3つを往来している。あるスコープから他のスコープのローカル変数は見えない。

v1 = 1
class MyClass
  v2 = 2
  local_variables # => [:v2]
  def my_method
    v3 = 3
    local_variables
  end
  local_variables # => [:v2]
end
obj = MyClass.new
obj.my_method # => [:v3]
puts local_variables # => [:v1, :obj]

スコープゲート

スコープが変化する場所は3つある。これらはスコープゲート (スコープの出入り口) として振る舞う。

  • クラス定義
  • モジュール定義
  • メソッド

フラットスコープ

ローカル変数はスコープゲートを超えられない。

my_var = "Hello, World!"

class MyClass
  # my_var をここに表示したい

  def my_method
    # my_var をここに表示したい
  end
end

Class.newdefine_method を使えばスコープをフラット化できる。この技法をフラットスコープと呼ぶ。

my_var = "Hello, World!"

MyClass = Class.new do
  puts my_var

  define_method :my_method do
    puts my_var
  end
end

puts MyClass.new.my_method

余談だけど JavaScriptRuby のようにブロックを使わなくても関数自体がクロージャとして働いている。

JavaScript の関数はクロージャとなるためです。クロージャは関数とその関数が作られた環境という 2 つのものの組み合わせです。 クロージャ - JavaScript | MDN

instance_eval

instance_eval はレシーバを self にしてから評価される。スコープは移らないのでローカル変数にもアクセスできる。

class MyClass
  def initialize
    @v = 1
  end
end

v = 2

obj = MyClass.new
obj.instance_eval do
  puts self # => #<MyClass:0x00000001007353b0>
  puts @v # => 1
  @v = v
  puts @v # => 2
end

呼び出し可能オブジェクト

コードを塊として保管しておき、あとから呼び出す方式には以下がある。

  • ブロック
  • Proc
  • lambda
  • メソッド

ブロックはこれまで触れた通り、他の3つを確認していく。

Proc

Proc はブロックをオブジェクトにしたもの。

z = 3
inc = Proc.new { |x| x + z }
puts inc.call(2)  # => 5

lambda

Proc オブジェクトを生成する別の方法。

dec = ->(x) { x - 1 }
puts dec.class # => Proc
puts dec.call(2) # => 1

Proc と lambda は「return の挙動」と「引数チェックの有無」に違いがある。

メソッド

メソッドも Method オブジェクトとして取り出し可能。

class MyClass
  def initialize(value)
    @x = value
  end

  def my_method
    @x
  end
end

obj = MyClass.new(1)
m = obj.method :my_method
puts m.class # => Method
puts m.call # => 1

ブロックや Proc が定義されたスコープで評価されるのに対し、メソッドはオブジェクトに束縛され、オブジェクトのスコープで評価される。

5章 木曜日: クラス定義

Ruby のクラス定義は実際に「コードを実行」している。

クラス定義

カレントクラス

クラス定義の中では、そのクラス自身がカレントオブジェクト self になる。それと同様に「カレントクラス」という概念も持っている。クラス内でメソッドを定義すると、それはカレントクラスのインスタンスメソッドとなる。

  • def で定義される全てのメソッドは、カレントクラスのインスタンスメソッドとなる
  • クラス定義の中では、「カレントオブジェクト self = カレントクラス」となる
  • クラスへの参照があれば class_eval でクラスをフラットスコープでオープンできる

クラスインスタンス変数

クラスは Class クラスのインスタンスであり、インスタンス変数を持つことができる。全てのインスタンス変数はカレントオブジェクト self に属している。クラスも例外ではない。

class MyClass
  @my_var = 1
  def self.read; puts @my_var; end
  def write; @my_var = 2; end
  def read; puts @my_var; end
end

obj = MyClass.new
obj.read # => nil
obj.write
obj.read # => 2
MyClass.read # => 1

このようにクラスに属するインスタンス変数を「クラスインスタンス変数」と呼ぶ。

クラス変数

ちなみに @@ プレフィックスをつけた「クラス変数」もある。クラスインスタンス変数とは異なり、サブクラスやインスタンスメソッドからもアクセスできる。さらに、クラス階層間で共有される特性がある。

class MyClass
  @@v = 1

  def self.read
    puts @@v
  end
end

MyClass.read # => 1

class SubClass < MyClass
  @@v = 2
end

MyClass.read # => 2
SubClass.read # => 2

特異メソッド

特定のオブエジェクトに追加したメソッドを「特異メソッド」と呼ぶ。特異メソッドは、オブジェクトのクラスに影響を与えない。つまりそのオブジェクトにのみ追加される。

str = 'hogehoge'

def str.title?
  self.upcase == self
end

puts str.title? # => false

クラスメソッドはクラスの特異メソッド

特異メソッドの構文は常にこうなる。

def object.method
  # メソッドの中身
end

クラスメソッドもこの構文に漏れない。つまりクラスメソッドはクラスの特異メソッドである。Class クラスのオブジェクトにメソッドを追加している、と言い換えても同じ。

class MyClass; end

def MyClass.read
  puts @v
end

def MyClass.write
  @v = 1
end

MyClass.write
MyClass.read

#### クラスマクロ クラス定義の中で便利に使えるクラスメソッドを「クラスマクロ」と呼ぶ。attr_* 族のようにクラス定義の中でキーワードのように便利に使えるものを指す。

特異クラス

特異メソッドはどこに定義されているのだろうか。オブジェクトはクラスへの参照を持つだけであり、インスタンスメソッドはクラスに定義されているはずだ。

def MyClass; end

obj = MyClass.new
def obj.my_method; end

obj.my_method # MyClass に my_method はない

この答えが「特異クラス」である。

class << an_object という特別な構文を使うことで、特異クラスのスコープに連れて行ってくれる。

obj = Object.new
puts obj.class # => Object

singleton_class = class << obj
  self
end

puts singleton_class.class # => Class

このような手続きを踏まなくても singleton_class メソッドを使うことで簡単に特異クラスを参照できる。

puts obj.singleton_class # => #<Class:#<Object:0x000000010097d5c8>>

特異クラスの特徴

  • Object#singleton_class や class << を使わないと見れない
  • 特異クラスはインスタンスを1つしか持てない
  • 継承ができない
  • 特異クラスはオブジェクトの特異メソッドの住処
  • 特異クラスは継承チェーンの一番下に置かれる

メソッド探索

class C
  def a_method
    'C#a_method()'
  end
end

class D < C; end

obj = D.new
obj.a_method # => "C#a_method()"

このコードのオブジェクトモデルの世界を表すと下図になる。

特異クラスをオープンしてインスタンスメソッドを追加する。

class << obj
  def a_method
    'D#a_method()'
  end
end

obj.a_method # => "D#a_method()"
obj.singleton_class # => #<Class:#<D:0x000000010092cad8>>
obj.singleton_class.class # => Class
obj.singleton_class.superclass # => D

これをオブジェクトモデル図に反映すると下図になる。

特異クラスとクラスメソッド

特異クラスとは特定のオブジェクトに追加されたメソッドが置かれる場所だった。クラスメソッドも同様に Class クラスのオブジェクトに特別に追加されたメソッド、つまり特異メソッドである。

クラスメソッドを上記のコードに追加してみる。

class C
  class << self
    def a_class_method
      'C.a_class_method'
    end
  end
end

C.a_class_method # => "C.a_class_method"

特異クラスとそのスーパークラスを訪ねてみる。

C.superclass # => Object
D.superclass # => C
C.superclass.superclass # => BasicObject
C.superclass.superclass.singleton_class # => #<Class:BasicObject>
C.singleton_class # => #<Class:C>
D.singleton_class # => #<Class:D>
D.singleton_class.superclass # => #<Class:C>
C.singleton_class.superclass # => #<Class:Object>

オブジェクトモデル図にまとめるとこのようになる。

特異クラスのスーパークラスが、スーパークラスの特異クラスになっている。どうしてこんな複雑なことをするのか。それはこう配置することでサブクラスからもクラスメソッドを呼び出せるようになるからだ。

D.a_class_method # => C.a_class_method

説明を付け加えると、クラス D がクラスメソッド a_class_method を実行するとき、それは D の特異クラス #D のインスタンスメソッドである。インスタンスメソッドは継承チェーンを上に登っていく。#D にないのであれば、次に見に行くのは...。

クラス拡張

クラスメソッドをモジュールでインクルードできるか。

module MyModule
  def self.my_method; puts 'hello'; end
end

class MyClass
  include MyModule
end

MyClass.my_method # => NoMethodError

なぜエラーになるかというと、クラスがモジュールをインクルードして得られるのはインスタンスメソッドだからだ。クラスメソッドを得るには、「特異クラスのインスタンスメソッド」にしなければならない。

module MyModule
  def my_method; puts 'hello'; end
end

class MyClass
  class << self
    include MyModule
  end
end

MyClass.my_method # => hello

my_method は MyClass の特異クラスのインスタンスメソッドである。つまり、my_method は MyClass のクラスメソッドになった。この技法を「クラス拡張」と呼ぶ。

わざわざ特異クラスをオープンしなくても、Object#extend を使えばよい。これはレシーバの特異クラスにモジュールをインクルードするためのショートカットである。

module MyModule
  def my_method; puts 'hello'; end
end

class MyClass
  extend MyModule
end

MyClass.my_method # => hello

6章 金曜日: コードを記述するコード

Kernel#eval

コードを文字列として実行して、その結果を返す。

arr = [10, 20]
element = 30
eval('arr << element') # => [10, 20, 30]

Binding オブジェクト

スコープをオブジェクトにして返す。Binding でスコープを取得すれば、そのスコープを持ち回ることができる。eval と組み合わせて後からそのスコープでコードを実行できる。

class MyClass
  def my_method
    @v = 1
    binding
  end
end

b = MyClass.new.my_method

eval '@v', b # => 1

irb は標準入力やファイルをパースして、各行を eval に渡している。Binding を使って異なるコンテキストでも実行できるようになっている。

# workspace.rb
eval(statements, @binding, file, line)

eval vs. block

Kernel#eval と class_eval や instance_eval は、コードを文字列で実行するか、ブロックとして実行するかの違いしかない、というのは誤りである。instance_eval もコードを文字列で評価できる。

ではどちらを使うべきなのか。基本的にはコード文字列を避けるべきである。

コード文字列を避けるべき理由

  • シンタックスハイライトや自動補完が効かない
  • コードインジェクションの標的になる

フックメソッド

クラスが継承されたときや新しいメソッドを獲得したとき、このようなイベントが起きたときに実行されるメソッドを「フックメソッド」と呼ぶ。イベントに「フックをかける」ことからこのように呼ばれる。

Class#inherited はクラスが継承されたときに Ruby が自動的に呼び出してくれる。デフォルトでは何もしないので、オーバーライドして使う。

class String
  def self.inherited(subclass)
    puts "#{self} was inherited by #{subclass}"
  end
end

class MyString < String; end

# Output:
# String was inherited by MyString

クラスのライフサイクルにプラグインする Class#inherited などと同様に、モジュールのライフサイクルにプラグインするものもある。

module M1
  def self.included(othermod)
    puts "M1 was included into #{othermod}"
  end
end

module M2
  def self.prepended(othermod)
    puts "M2 was prepended to #{othermod}"
  end
end

class C
include M1
prepend M2
end

# Output:
# M1 was included into C
# M2 was prepended to C

フックメソッドを活用した最終的なサンプルコードはこのようになる。

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_variable_set('@#{attribute}', value)
      end

      define_method attribute do
      instance_variable_get("@#{attribute}")
      end
    end
  end
end

class Person
  include CheckedAttributes
  
  attr_checked :age do |v|
    v >= 18
  end
end

Ⅱ部

9章 Active Record の設計

オートローディング

require 'active_record' したときに読み込まれるファイル。

github.com

Active Record は Active Model と Active Support の2つのライブラリに大きく依存している。Active Support::Autoload モジュールを extend して autoload をクラスマクロとして使用する。これはモジュールを初めて呼び出したときに自動的にソースコードを require するというもの。これにより active_record を require するだけで配下の様々なモジュールを利用できる。

ActiveRecord::Base

ActiveRecord::Base にロジックはなくモジュールを include あるいは extend するだけ。オートローディングの仕組みによって require してからモジュールを include する必要がない。

github.com

ActiveRecord::Validations

ActiveRecord::Base クラスは ActiveRecord::Validations モジュールを include している。valid? メソッドはここで定義されている。

github.com

10章 Active Support の Concern モジュール

ActiveSupport::Concern モジュールがあることで、クラスが include するモジュールにいちいちフックメソッドを定義しなくてよくなる。

Concern 以前の Rails

ActiveRecord::BaseValidations を include すると以下のことが起きる。

module ActiveRecord
  module Validations
    def self.included(base)
      base.extend ClassMethods
    end

    module ClassMethods
      def validates_length_of(*args)
        # ...
      end
    end

    def valid?
      # ...
    end
  end
end

このコードの課題は、モジュールに重複したフックメソッドが定義されること。

class Base
  include Validations
  extend Validations::ClassMethods
  # ...
end

このように書けば同じ目的を達成できる。extend の1行は追加されるが問題ないと思うかもしれない。これにはもっと深刻な問題が隠されている。それは、モジュールを入れ子で include したときに起きる。

module SecondLevelModule
  def self.included(base)
    base.extend ClassMethods
  end

  module ClassMethods
    def second_level_class_method
      "Second level class method"
    end
  end

  def second_level_instance_method
    "Second level instance method"
  end
end

module FirstLevelModule
  def self.included(base)
    base.extend ClassMethods
  end

  module ClassMethods
    def first_level_class_method
      "First level class method"
    end
  end

  def first_level_instance_method
    "First level instance method"
  end
end

class BaseClass
  include FirstLevelModule
end

BaseClass.new.first_level_instance_method # => "First level instance method"
BaseClass.new.second_level_instance_method # => "Second level instance method"

BaseClass.first_level_class_method # => "First level class method"
BaseClass.second_level_class_method # => NoMethodError

second_level_class_methodBaseClass のクラスメソッドではなく、FirstLevelModule のクラスメソッドとなる。

ActiveSupport::Concern

クラスメソッドを定義するためにフックメソッドを定義しなくてもよくなる。

require 'active_support'

module MyConcern
  extend ActiveSupport::Concern

  def an_instance_method; "an instance method"; end

  module ClassMethods
    def a_class_method; "a class method"; end
  end
end

class BaseClass
  include MyConcern
end

BaseClass.new.an_instance_method # => "an instance method"
BaseClass.a_class_method # => "a class method"

ActiveModel::Validations

validateActiveRecord::Base クラスのクラスメソッド (クラスマクロ) として利用される。ソースコードより ActiveSupport::Concern を extend し ClassMethods モジュールに validate メソッドが定義されていることが分かる。

github.com