Rubyのブロック(クロージャ)はローカル変数をインスタンス変数に変えるマジックだ!

ブログを下記に移転しました。デザイン変更により移転先では記事が一層読みやすくなっていますので、よろしければ移動をお願い致します。

Rubyのブロック(クロージャ)はローカル変数をインスタンス変数に変えるマジックだ! : melborne.github.com

                                                                                    • -

Ruby クロージャ - ソースコード備忘録を読んで
自分のRubyにおけるブロックと変数の理解が怪しいことがわかった
でちょっと普通とは違うアプローチからの整理を試みて
理解できた気がするので書いてみます
誤りを教えてくれればうれしいです

ローカル変数

プログラムコードはプログラマの意思をインタプリタに伝えるものだ
Rubyでは
オブジェクトに処理を依頼する形でプログラムを組成するけど
そのとき変数はプログラマ
対象のオブジェクトを指し示すためのラベルとして用いられる
すなわち変数はオブジェクト参照ラベルだ


複数の手続きブロックで構造化される
現代のプログラミングにおいては
1つの変数の適用範囲は
それが定義されている手続きブロックに限定されるのが普通だ


Rubyにおいてメソッド定義は
この手続きブロックを新たに作る
だから以下の例でcounterメソッド内の変数nは
未定義となる

 n = 0
 def counter
   n += 1
 end
 
 counter # => 
 # ~> -:3:in `counter': undefined method `+' for nil:NilClass (NoMethodError)
 # ~> 	from -:6

つまりトップレベルとcounterメソッドとは
別の手続きブロックであり
トップレベルで定義した変数nは
counterメソッド内で参照できない


このようにその適用範囲が
それが定義されている手続きブロックに限定される変数は
ローカル変数と呼ばれる


メソッドの壁を越えて
ローカル変数を参照できるようにするには
変数をメソッドの引数として渡す必要がある

 n = 0
 def counter(x)
   x + 1
 end
 n = counter(n) # => 1
 n = counter(n) # => 2
 n = counter(n) # => 3

インスタンス変数

一方Rubyには
インスタンス変数という適用範囲がより広い変数がある
インスタンス変数は手続きブロックを超えて
それが定義されるオブジェクトの範囲で有効となる変数だ
つまりRubyインタプリタ
インスタンス変数の有効範囲に関し
オブジェクトを一つの手続きブロックとみなす


だから上のコードのローカル変数nを
インスタンス変数@nに変えれば
counterメソッドから変数@nが見える

 @n = 0
 def counter
   @n += 1
 end
 
 counter # => 1
 counter # => 2
 counter # => 3
 
 instance_variable_get :@n # => 3

ここでRubyのトップレベルは
Objectクラスのインスタンスであるmainオブジェクトの
コンテキストを持っているので
上のコードはインスタンス変数nに関し以下と等価だ

 class Object
   def initialize
     @n = 0
   end
   def counter
     @n += 1
   end
 end
 
 main = Object.new
 main.counter # => 1
 main.counter # => 2
 main.counter # => 3
 
 main.instance_variable_get :@n # => 3

上のコードにより
1つのメソッド内で定義されたインスタンス変数@nが
オブジェクト全体で有効であることがよりはっきり分かる


前に書いたように
インスタンス変数は変数の有効範囲をオブジェクトにまで拡張する
しかしその一方でそのクラスまでは拡張しない
つまり同一のクラスから生成される複数のオブジェクト間で
インスタンス変数が共有されることはないんだ

 class Object
   def initialize
     @n = 0
   end
   def counter
     @n += 1
   end
 end
 
 main = Object.new
 main.counter # => 1
 main.counter # => 2
 main.counter # => 3
 main.instance_variable_get :@n # => 3
 
 main2 = Object.new
 main2.counter # => 1
 main2.counter # => 2
 main2.instance_variable_get :@n # => 2

ここでObjectクラスのmainオブジェクトとmain2オブジェクトは
それぞれがインスタンス変数@nを持つけれども
それらの有効範囲は
それぞれのオブジェクト内に限られていることがわかる

クラス変数

Rubyにはさらに有効範囲の広い変数がある
@@ではじまるクラス変数だ
クラス変数はその有効範囲をクラスにまで拡張する
つまりRubyインタプリタはクラス変数の有効範囲に関し
クラスを一つの手続きブロックとみなす

 class Object
   def initialize
     @@n ||= 0
   end
   def counter
     @@n += 1
   end
 end
 
 main = Object.new
 main.counter # => 1
 main.counter # => 2
 main.counter # => 3
 
 main2 = Object.new
 main2.counter # => 4
 main2.counter # => 5

この例からObjectクラスの複数のインスタンスmain,main2間で
1つのクラス変数@@nが共有されていることがわかる


変数の有効範囲が広がると
プログラマが予期しない問題が起こることがある
Rubyではクラス変数の有効範囲が
サブクラスのインスタンスにも拡張する点留意が必要だ

 class Object
   def initialize
     @@n ||= 0
   end
   def counter
     @@n += 1
   end
 end
 
 main = Object.new
 main.counter # => 1
 main.counter # => 2
 main.counter # => 3
 
 main2 = Object.new
 main2.counter # => 4
 main2.counter # => 5
 
 'string'.counter # => 6
 [].counter # => 7
 class MyClass; end
 MyClass.new.counter # => 8

この例ではObjectクラスにクラス変数が定義され
それがメモリ上のすべてのクラスのインスタンス
参照・変更できることが示されている
この場合クラス変数はグローバル変数とほぼ等価になる
だからRubyのトップレベルでクラス変数を定義するときは
グローバル変数を定義していると理解したほうがいい


変数の有効範囲をインスタンスやサブクラスに広げずに
それが定義されたクラスオブジェクトに限定したい場合
クラスオブジェクトの文脈でインスタンス変数が使える

 class Super
   @n = 0
   def self.counter
     @n += 1
   end
 end

 class Sub < Super; end

 Super.counter # => 1
 Super.counter # => 2
 Super.counter # => 3

 Sub.counter # => # !> instance variable @n not initialized

 main = Super.new
 main.counter # => undefined method

クロージャ

Rubyには{}またはdo endで挟むことによって
手続きのまとまりを表現するブロックという
メソッド類似の構文がある
ブロックはそれ単独ではメモリ上に存在できない

 n = 0
 { n += 1 } # => Error

だけれどもメソッドに伴われるかたちなら存在できるようになる

 n = 0
 1.times { n += 1 }
 1.times { n += 1 }
 n # => 2

Rubyのブロックはメソッドによる手続きブロックとは異なって
ブロックの外側で定義されたローカル変数を
ブロック内で参照・変更できるという性質を有する*1


さらにRubyのブロックはそれ自身をオブジェクト化することができ
そうすることによってメモリ上に独立して存在できるようになる

 lambda { n += 1 } # => #<Proc:0x0001f57c@-:27>

ブロックをオブジェクト化したものは
Procクラスのインスタンスであり
callメソッドを呼ぶことによって
ブロック内の手続きを呼び出すことができる
そしてこの場合でもブロックがその外側で定義された
ローカル変数を参照できるという性質は保たれる
このようなブロックの性質はクロージャと呼ばれる

 n = 0
 main = lambda { n += 1 }
 main.call # => 1
 main.call # => 2
 main.call # => 3

これは先に示したインスタンス変数の例と良く似ている
callメソッドを別名定義すれば類似性がよりはっきりする

 n = 0
 main = lambda { n += 1 }
 def main.counter
   self.call
 end
 main.counter # => 1
 main.counter # => 2
 main.counter # => 3
 
 #インスタンス変数の例
 class Object
   def initialize
     @n = 0
   end
   def counter
     @n += 1
   end
 end
 
 main = Object.new
 main.counter # => 1
 main.counter # => 2
 main.counter # => 3

ここでローカル変数nはインスタンス変数@nのように機能し
オブジェクトmainの状態を保持している
つまりブロックによって
ローカル変数がインスタンス変数のように働いている


そう
つまりブロック(クロージャ)は
ローカル変数をインスタンス変数に変えるマジックなんだ!


(追記:2009/8/27) ブロックにおける変数の有効範囲の説明を訂正しました。
(追記:2009/8/27) クラスオブジェクトでインスタンス変数を使う説明を追加しました。

*1:ただブロック内で定義された変数はその外側で参照できない