Rubyのスコープとクロージャ

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を共有することができません。