Posted at

Ruby: Binding

More than 3 years have passed since last update.


はじめに

Binding クラス「class Binding (Ruby 2.1.0 リファレンスマニュアル)」には以下のような要約が記述されています。


ローカル変数のテーブルと self、モジュールのネストなどの情報を保 持するオブジェクトのクラスです。


分かったこと・調べたことをメモしました。


クロージャ

クロージャを例に Binding を考えていきます。

以下は Ruby におけるクロージャの例です。

a = 10

p [1,2,3].map {|n| n * a } #=> [10, 20, 30]

ここで、クロージャが以下の3つから成ると考えます。


  • 包んでいるもの(クロージャ)。上の例ではブロック「{|n| n * a}

  • 包まれているもの(変数)。上の例では変数「a

  • 包まれているものがあるところ(環境)。上の例では「変数aが定義されているスコープ(明示されてないがここでは main)」

(イメージ図)

ブロック、変数、スコープとも字義的な面での説明です。

オブジェクト指向の Ruby 世界では、プログラムにあるものはオブジェクトです。

ブロック(クロージャ)は Proc オブジェクトになります。

そして、スコープ(環境)が Binding オブジェクトになります。

変数は Binding オブジェクトに保持されます。


変数

Binding オブジェクトに保持された変数が評価されるのは、クロージャの実行時です。

以下の例で、もしも、変数a がクロージャ定義時に評価されたとしたら、最後に「20」が出力されるはずですが、そうはなりません。

a = 10                # main に変数`a`を定義する。

f1 = -> { a * 2 } # クロージャは変数`a`を包む。`a`の参照するオブジェクト「`10`」を包むのではない

a = 100 # 変数`a`の参照するオブジェクトを変更
p f1.() #=> 200 # クロージャ実行時に`a`が参照するオブジェクト「`100`」を評価

以下の例では、ブロック内の変数bと main の変数bは別物であり、クロージャ実行時にエラーになります。

クロージャが包むのは、クロージャ定義時に定義済みのブロック外の変数です。

f2 = -> { b * 2 }  # 変数`b`はブロック内の変数

b = 1000 # main に変数`b`を定義する。しかし、ブロック内の変数`b`とは別物。
# さらに、クロージャ定義時にはこの変数は定義されてないので、クロージャに包まれていない。
p f2.() # NameError: undefined local variable or method `b' for main:Object 。。。(;_;)

(イメージ図)

まとめると、クロージャは、


  • 「クロージャ定義時」に定義されているブロック外変数を包む

  • その変数が評価されるのは、「クロージャの実行時」

です。

クロージャ定義時に変数を包んでおいて実行時に評価したいが、変数自体はオブジェクトではありません。

そこで、スコープ(環境)をオブジェクト化してそこに変数を保持した。そのオブジェクトが Binding と考えられます。


高階関数

Ruby には関数はありません。なので、高階関数もありません。

。。。という細かい話は置いといて、ここでは、feeling 的に「高階関数」と呼んでおきます。

高階関数の例です。

g = -> { a = 10 ; -> { a * 2 } }  # g は関数 (-> { a * 2 }) を返す高階関数

f = g.()
f.() #=> 20

包まれている変数のスコープ(環境)は g のローカルスコープです。

それ以外は、前述の例と同様です。

高階関数定番(?)の例です。

ここではカウンタは Array にしています。カウントアップ毎に :x が増えます。

g = -> { count = [] ; -> { count << :x } }

f1 = g.() # f1 のコール f2 のコール
f1.() #=> [:x] # 1回目
f1.() #=> [:x, :x] # 2回目
f1.() #=> [:x, :x, :x] # 3回目

f2 = g.()
f2.() #=> [:x] # 1回目
f2.() #=> [:x, :x] # 2回目

f1.() #=> [:x, :x, :x, :x] # 4回目

(イメージ図)

クロージャが包むのは定義時の(変数を含む)環境です。(上では g のスコープ)

f1 と f2 が作られる(クロージャ定義される)時、それぞれ g が実行されます。

その度に ローカル変数 count に Array オブジェクトが作られるので、f1 と f2 が包んでいる count(が参照するArrayオブジェクト)は別物になります。


binding

Binging オブジェクトは Kernel.#binding で取り出せます。

高階関数の例の f1 の Bindig オブジェクトを取り出してみます。

b1 = f1.binging

p b1.class #=> Binding

Binding#local_variable_get, Binding#local_variable_set でローカル変数の取得、設定ができます。

p b1.local_variable_get(:count)    #=> [:x, :x, :x, :x]

b1.local_variable_set(:count, []) # カウンタをリセットしてみる
p f1.() #=> [:x]
p f1.() #=> [:x, :x]

[][]= などにエイリアスすると使いやすいかもしれません。

class Binding

alias [] local_variable_get
alias []= local_variable_set
end

g = -> { a = 0 ; -> { a += 1 } }
f = g.()

f.() #=> 1
f.() #=> 2
f.() #=> 3

b = f.binding
p b[:a] #=> 3
b[:a] = 10 # 変数`a` の値を 10 にする

f.() #=> 11
f.() #=> 12

なお、Binding#local_variable_get で取得できる変数はクロージャが包んだ変数です。

クロージャのブロック内(実行時スコープ)の変数は取得できません。

g = -> { a = 10; -> { x = 2; a * 2 } }

f = g.()
b = f.binding

b.local_variable_get :a #=> 10
b.local_variable_get :x # エラー (NameError: local variable `x' not defined for #<Binding:...)

Proc 以外のオブジェクトも Binding をもっています。

ただし、binding メソッドは private になっていることが多いようです。

obj = Object.new

b = obj.send(:binding) # private メソッドだが、send でなら呼び出せる
p b.class #=> Binding


補足

本稿で使ってきた言葉ですが、

-「クロージャ定義時に見えているスコープ」=> 「レキシカルスコープ(lexical scope)」

-「クロージャが包んでいる変数」=>「レキシカル変数(レキシカルスコープの変数)」

-「レキシカルスコープの持ち主(上の例の main や g)」=>「エンクロージャ(enclosure)」

といったりします。

これらは Ruby に限らない用語ですが、どの程度一般的かなど詳しいまではよく分かりません。m(_ _)m

何か調べる際の参考にしてください。


余談

ブロックは、レキシカル変数を捕らえてクロージャとして機能したりしますが、実行時(ダイナミックな)スコープを利用する用法もあります。

v = 10

# instance_eval は self をレシーバにしてブロックを評価する
instance_eval do # ここでは、レシーバが self なので self を self にしている。

a = 1 # ブロック内(ダイナミックスコープ)の変数
b = 2 # ブロック内(ダイナミックスコープ)の変数

p a #=> 1
p b #=> 2
p v #=> 10 # ブロック外(レキシカルスコープ)の変数
end

p v #=> 10
p a # エラー! (NameError: undefined local variable or method `a' ...)
p b # エラー! (NameError: undefined local variable or method `b' ...)

上の変数 ab は instance_eval のブロック内だけの変数です。ブロックを抜けると未定義になります。

現在の環境を一時的な変数などで汚したくない等の場合(私は irb や pry などを使っている時に、たまにあります)は、上のようなことができます。


おわりに

本稿内容の動作確認は以下の環境で行っています。


  • Ruby 2.1.5 p273