感想
書くとき、読むとき、レビューするときに、そのコードの解像度が広がるというか、奥に広がる世界にまで意識が届くようになった。読んでいても知識を押し付けられる感覚がないので、楽しみながら 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 # この時点では @v は存在しない obj.my_method # はじめて @v が存在できる
メソッド
オブジェクトにはメソッドはなく、インスタンス変数とクラスへの参照があるだけだ。
インスタンスそのものにメソッドが定義されているわけではない。ただ、「クラス MyClass が my_method を持つ」というのも誤解がある。この明確な呼び分けとして「インスタンスメソッド」と「クラスメソッド」がある。MyClass#my_method
と MyClass.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.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 # => #<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'
したときに読み込まれるファイル。
Active Record は Active Model と Active Support の2つのライブラリに大きく依存している。Active Support::Autoload
モジュールを extend して autoload
をクラスマクロとして使用する。これはモジュールを初めて呼び出したときに自動的にソースコードを require するというもの。これにより active_record
を require するだけで配下の様々なモジュールを利用できる。
ActiveRecord::Base
ActiveRecord::Base
にロジックはなくモジュールを include あるいは extend するだけ。オートローディングの仕組みによって require してからモジュールを include する必要がない。
ActiveRecord::Validations
ActiveRecord::Base
クラスは ActiveRecord::Validations
モジュールを include している。valid?
メソッドはここで定義されている。
10章 Active Support の Concern モジュール
ActiveSupport::Concern
モジュールがあることで、クラスが include するモジュールにいちいちフックメソッドを定義しなくてよくなる。
Concern 以前の Rails
ActiveRecord::Base
が Validations
を 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_method
は BaseClass
のクラスメソッドではなく、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
validate
は ActiveRecord::Base
クラスのクラスメソッド (クラスマクロ) として利用される。ソースコードより ActiveSupport::Concern
を extend し ClassMethods
モジュールに validate
メソッドが定義されていることが分かる。