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) クラスオブジェクトでインスタンス変数を使う説明を追加しました。

(comment) トラックバックありがとうございます。
>しかしメソッドによる手続きブロックとは異なって
>Rubyのブロックは変数の新たな有効範囲を作らない
>つまり上の例のようにブロックの外側で定義された
>ローカル変数nをブロック内で参照・変更できる
ブロックでも変数の新たなスコープはできますよ。
参考URL http://www.ruby-lang.org/ja/man/html/FAQ_CAD1BFF4A1A2C4EABFF4A1A2B0FABFF4.html
ただ、その時にクロージャがあることによってブロックを定義した環境のローカル変数を使えるようになるのではないでしょうか。
>m = 1
>func1 = lambda {
> n = 3
> p n, m
> }
>func1.call
>p m
>p n
の場合、最後の行はエラーになるとおもいます。
ブロックが変数のスコープを作らないのなら、
エラーがでないことにならないでしょうか。
rubyは勉強中の身なので間違っているかもしれませんが、
参考になれば幸です。

yuki rinrinさん
そうですね、ブロックが変数の新たなスコープを作らないというのは言いすぎでした。ただ、個人的にはそれが外側のローカル変数を参照できる限り、新たなスコープを作るとは言いづらいんですよね。いずれにしても、明らかな間違いなので本文訂正しました。ご指摘ありがとうございます。

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


blog comments powered by Disqus
ruby_pack8

100円〜で好評発売中!
M'ELBORNE BOOKS