Rubyのスコープとクロージャ
スコープとは
スコープとは、プログラム1行1行が住んでいる環境のことです。
# sample.rb x = 1 def calc x += 1 end puts calc # => undefined local variable or method "x"
上の例では、トップレベル(クラスもメソッドも定義していないまっさらな場所)で変数x(ローカル変数)を定義していますが、 変数xは、その下に定義されたcalcメソッドの中で利用することができなくなっています。 これはなぜかというと、トップレベルのスコープとcalcメソッド中のスコープが異なるからです。
つまり、ここにはトップレベルのスコープ(def〜endを除く部分)と、calcメソッドのスコープ(def〜endの中)が存在するのです。
今回はメソッドでしたが、クラス定義の中にも同様のスコープが存在します。 そして、そのクラス定義の中のメソッド定義の中にも、異なるスコープが存在するわけです。
クロージャとは
Rubyのブロックはクロージャです。 クロージャを使うということは、すなわち、ブロックを使うということです。 クロージャ(ブロック)では、メソッドにはできなかったローカル変数の参照ができるようになります。
# sample.rb x = 1 def calc yield + 1 end puts calc { x += 10 } # => 12 puts x # => 11 puts calc { x += 10 } # => 22 puts x # => 21
上記の例では、トップレベルに定義されたローカル変数xを、 メソッドcalcに渡したブロックの中で加算しています。
ブロックはメソッドcalcに渡され、 ブロック内の加算結果に、メソッド内で+1をしています。 { x += 10 } + 1なので、つまり12です。
また、クロージャの性質として、クロージャ(ブロック)の中で更新された変数はそれ以降も参照されるようになります。
ですので、 puts calc { x += 10 } の後に puts x をしてみると、ブロックの中の計算結果が、計算以後も保持されていることを確認することができます。
ですが、クロージャであるブロック自身が参照するローカル変数にも限度があります。
# sample.rb x = 1 class Block x = 1 def calc(&block) yield + 1 end def calcalc self.calc{ x += 10 } end end block = Block.new puts block.calcalc # => Error
上記のように、 block.calcalcを呼び出すとエラーになります。
クロージャ(ブロック)はこのように、2段階上のスコープと1段階上のスコープにあるローカル変数を読み込めません。
# sample.rb class Block def calc(&block) yield + 1 end def calcalc # 代わりにここにxを記述して、 x = 1 # 以下にxを閉じ込めたブロックをcalcメソッドに渡す。 calc{ x += 10 } end end block = Block.new puts block.calcalc # => 12
こうすると読み込まれるようになりました。 クロージャ(ブロック)が参照できる範囲は、そのクロージャが存在するスコープ内に限定されるわけです。 { x += 10 }(11)をcalcメソッドに渡して、そのメソッド内で、さらに+1されたので12となります。
クロージャとは、同じスコープにあるローカル変数を参照し、 それを本来使うことができないメソッドなどに渡すことができるのです。
スコープのフラット化
では、このようなスコープとクロージャの知識をどのように使うことができるのでしょうか?
利用例の一部を紹介すると、スコープを飛び越えて、変数を利用することができるようになります。
# sample.rb x = 1 class Flat puts x * 100 def use_x x * 10 end end flat = Flat.new puts flat # => Error
上記のコードを実行すると、xがありません、とエラーを起こします。 ですが、ここでクロージャ、すなわちブロックを利用すると、スコープを飛び越えることができるようになります。
クラス定義の方法には、ブロックを利用した方法もあります。
# sample.rb Flat = Class.new do puts x * 100 def use_x x * 10 end end
上記のコードでは、クラスを作成したのち、Flatという定数に代入しています。 そもそも、クラスの名前とはただの定数なのであり、上記のようにClass.newを代入しているにすぎません。
このブロックを使ったクラスの作成方法を利用して、トップレベルのローカル変数をクラスのスコープの中でも参照できるようにします。
# sample.rb x = 1 Flat = Class.new do puts x * 100 def use_x x * 10 end end flat = Flat.new puts flat # => 100 puts flat.use_x # => Error
無事に、クラスの中でもトップレベルで定義したローカル変数xを参照できるようになりました。
ですが、インスタンスメソッド内では、依然としてxを参照できないようです。 ですので、クラスと同じように、インスタンスメソッド定義において、ブロックを利用する書き方を利用してみましょう。
# sample.rb x = 1 Flat = Class.new do puts x * 100 define_method :use_x do x * 10 end end flat = Flat.new puts flat # => 100 puts flat.use_x # => 10
define_methodは、言葉そのままで、メソッドを定義するメソッドです。 シンボルにメソッド名を渡し、ブロック内にロジックを記述します。
define_methodは、動的にメソッドを定義するときなどにも使います。
# sample.rb class Sample def self.dynamic_method(name) define_method(name) do name + "is successfully defined." end end end puts Sample.instance_methods.grep(/testing/) # => No results Sample.dynamic_method :testing puts Sample.instance_methods.grep(/testing/) # => testing
最初に定義していなくても、このように、あとでメソッドを動的に定義することができます。
とにかく、クロージャ(ブロック)を利用して、スコープを飛び越えてローカル変数を参照できるようにするコードをご紹介しました。 この一連の作業は、スコープをフラット化する、と表現し、また、この技術をフラットスコープと呼びます。
クラスやメソッドのスコープにこの変数を持ち込みたいなあ、と思った時には、フラットスコープを利用して、変数を手渡してあげましょう。
スコープの共有
また、以上のことから、スコープの共有を行うことができます。
# sample.rb class Flat x = 1 define_method :first_calc do x * 10 end define_method :second_calc do x * 20 end def third_calc puts "ここではxを参照することはできません。" end end flat = Flat.new puts flat.first_calc # => 10 puts flat.second_calc # => 20 puts flat.third_calc # => ここではxを参照することはできません。
このコードから、first_calcとsecond_calcのメソッドは、Flatクラスのスコープにあるローカル変数xを共有していることがわかります(スコープの共有)。 third_calcはクロージャを使っていないので、xを参照することができませんから、xを共有することができません。