感想
書くとき、読むとき、レビューするときに、そのコードの解像度が広がるというか、奥に広がる世界にまで意識が届くようになった。読んでいても知識を押し付けられる感覚がないので、楽しみながら Ruby の奥深さを学ぶことができる。
Ⅰ部
1章 頭文字 M
メタプログラミングとは、コードを記述するコードを記述することである。
C++ のようなコンパイル型の言語では、コンパイルすると変数やメソッドはその実体を失う。コンパイル後にインスタンスメソッドのことをクラスに質問できない。Ruby のようなインタプリタ型の言語では、あらゆる言語要素 (変数、メソッド、クラス等) が実行時にも存在している。irb のような対話的にコードを打ち込めるシェルを使っていると、この辺りは実感としてはある。
Active Record は Movie#title
や Movie#director=
といったメソッドをこっそり定義している。
class Movie < ActiveRecord::Base
end
Active Record は目に触れる機会の多いメタプログラミングなのかもしれない。attr_reader
とかも暗黙的にメソッドを定義しているので身近なメタプログラミングと言えるのだろうか。
2章 月曜日: オブジェクトモデル
いつでも既存のクラスを再オープンして、その場で修正できる。この技法をオープンクラスと呼ぶ。
オブジェクトの中身
class MyClass
def my_method
@v = 1
end
end
obj = MyClass.new
obj.my_method
メソッド
オブジェクトにはメソッドはなく、インスタンス変数とクラスへの参照があるだけだ。
インスタンスそのものにメソッドが定義されているわけではない。ただ、「クラス MyClass が my_method を持つ」というのも誤解がある。この明確な呼び分けとして「インスタンスメソッド」と「クラスメソッド」がある。MyClass#my_method
と MyClass.my_method
のように表記する。
クラスの真相
Ruby のオブジェクトモデルを学ぶときに最も重要なのは「クラスはオブジェクト」ということだろう。
クラスはオブジェクトであり、クラスにもクラスがある。クラスのクラスは Class
である。
String.class
Class クラスのインスタンスにはメソッドがある。
Class.instance_methods(false)
new
なんかは分かりやすい。
Array クラスは Object クラスを継承している。つまり「配列はオブジェクトである」と言うことができる。
Array.superclass
Ruby ではあらゆるクラスのスーパークラスは Object になる。Object クラスは to_s
のようなあらゆるオブジェクトで便利に使えるメソッドを持っている。
モジュール
Class のスーパークラスは Module だ。
Class.superclass
これはちょっと意外だった。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)
ゴーストメソッド
オブジェクトにメソッドが見つからなければ、元のオブジェクトの 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
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}
例外が発生しても実行しなければいけない処理をシンプルに書くこともできる。
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" }
x
は「ブロックを定義したとき」に束縛される。ブロックからメソッドのローカル変数である x
は見えない。
スコープ
local_variables
を使ってローカル変数を確認することで、スコープの遷移を追跡できる。このコードでは、「トップレベルのスコープ」「MyClass のスコープ」「my_method のスコープ」の3つを往来している。あるスコープから他のスコープのローカル変数は見えない。
v1 = 1
class MyClass
v2 = 2
local_variables
def my_method
v3 = 3
local_variables
end
local_variables
end
obj = MyClass.new
obj.my_method
puts local_variables
スコープゲート
スコープが変化する場所は3つある。これらはスコープゲート (スコープの出入り口) として振る舞う。
フラットスコープ
ローカル変数はスコープゲートを超えられない。
my_var = "Hello, World!"
class MyClass
def my_method
end
end
Class.new
や define_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
余談だけど JavaScript は Ruby のようにブロックを使わなくても関数自体がクロージャとして働いている。
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
puts @v
@v = v
puts @v
end
呼び出し可能オブジェクト
コードを塊として保管しておき、あとから呼び出す方式には以下がある。
ブロックはこれまで触れた通り、他の3つを確認していく。
Proc
Proc はブロックをオブジェクトにしたもの。
z = 3
inc = Proc.new { |x| x + z }
puts inc.call(2)
lambda
Proc オブジェクトを生成する別の方法。
dec = ->(x) { x - 1 }
puts dec.class
puts dec.call(2)
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
puts m.call
ブロックや 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
obj.write
obj.read
MyClass.read
このようにクラスに属するインスタンス変数を「クラスインスタンス変数」と呼ぶ。
クラス変数
ちなみに @@
プレフィックスをつけた「クラス変数」もある。クラスインスタンス変数とは異なり、サブクラスやインスタンスメソッドからもアクセスできる。さらに、クラス階層間で共有される特性がある。
class MyClass
@@v = 1
def self.read
puts @@v
end
end
MyClass.read
class SubClass < MyClass
@@v = 2
end
MyClass.read
SubClass.read
特異メソッド
特定のオブエジェクトに追加したメソッドを「特異メソッド」と呼ぶ。特異メソッドは、オブジェクトのクラスに影響を与えない。つまりそのオブジェクトにのみ追加される。
str = 'hogehoge'
def str.title?
self.upcase == self
end
puts str.title?
クラスメソッドはクラスの特異メソッド
特異メソッドの構文は常にこうなる。
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
この答えが「特異クラス」である。
class << an_object
という特別な構文を使うことで、特異クラスのスコープに連れて行ってくれる。
obj = Object.new
puts obj.class
singleton_class = class << obj
self
end
puts singleton_class.class
このような手続きを踏まなくても singleton_class
メソッドを使うことで簡単に特異クラスを参照できる。
puts obj.singleton_class
特異クラスの特徴
- 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
このコードのオブジェクトモデルの世界を表すと下図になる。

特異クラスをオープンしてインスタンスメソッドを追加する。
class << obj
def a_method
'D#a_method()'
end
end
obj.a_method
obj.singleton_class
obj.singleton_class.class
obj.singleton_class.superclass
これをオブジェクトモデル図に反映すると下図になる。

特異クラスとクラスメソッド
特異クラスとは特定のオブジェクトに追加されたメソッドが置かれる場所だった。クラスメソッドも同様に Class クラスのオブジェクトに特別に追加されたメソッド、つまり特異メソッドである。
クラスメソッドを上記のコードに追加してみる。
class C
class << self
def a_class_method
'C.a_class_method'
end
end
end
C.a_class_method
特異クラスとそのスーパークラスを訪ねてみる。
C.superclass
D.superclass
C.superclass.superclass
C.superclass.superclass.singleton_class
C.singleton_class
D.singleton_class
D.singleton_class.superclass
C.singleton_class.superclass
オブジェクトモデル図にまとめるとこのようになる。

特異クラスのスーパークラスが、スーパークラスの特異クラスになっている。どうしてこんな複雑なことをするのか。それはこう配置することでサブクラスからもクラスメソッドを呼び出せるようになるからだ。
D.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
なぜエラーになるかというと、クラスがモジュールをインクルードして得られるのはインスタンスメソッドだからだ。クラスメソッドを得るには、「特異クラスのインスタンスメソッド」にしなければならない。
module MyModule
def my_method; puts 'hello'; end
end
class MyClass
class << self
include MyModule
end
end
MyClass.my_method
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
6章 金曜日: コードを記述するコード
Kernel#eval
コードを文字列として実行して、その結果を返す。
arr = [10, 20]
element = 30
eval('arr << element')
Binding オブジェクト
スコープをオブジェクトにして返す。Binding でスコープを取得すれば、そのスコープを持ち回ることができる。eval と組み合わせて後からそのスコープでコードを実行できる。
class MyClass
def my_method
@v = 1
binding
end
end
b = MyClass.new.my_method
eval '@v', b
irb は標準入力やファイルをパースして、各行を eval に渡している。Binding を使って異なるコンテキストでも実行できるようになっている。
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
クラスのライフサイクルにプラグインする 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
フックメソッドを活用した最終的なサンプルコードはこのようになる。
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
にロジックはなくモジュールを include あるいは extend するだけ。オートローディングの仕組みによって require してからモジュールを include する必要がない。
github.com
ActiveRecord::Base
クラスは ActiveRecord::Validations
モジュールを include している。valid?
メソッドはここで定義されている。
github.com
10章 Active Support の Concern モジュール
ActiveSupport::Concern
モジュールがあることで、クラスが include するモジュールにいちいちフックメソッドを定義しなくてよくなる。
Concern 以前の Rails
ActiveRecord::Base
が Validations
を include すると以下のことが起きる。
Validations
のインスタンスメソッドが Base
クラスのインスタンスメソッドになる
ClassMethods
のインスタンスメソッドが Base
クラスのクラスメソッドになる
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
BaseClass.new.second_level_instance_method
BaseClass.first_level_class_method
BaseClass.second_level_class_method
second_level_class_method
は BaseClass
のクラスメソッドではなく、FirstLevelModule
のクラスメソッドとなる。
クラスメソッドを定義するためにフックメソッドを定義しなくてもよくなる。
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
validate
は ActiveRecord::Base
クラスのクラスメソッド (クラスマクロ) として利用される。ソースコードより ActiveSupport::Concern
を extend し ClassMethods
モジュールに validate
メソッドが定義されていることが分かる。
github.com