実務でブロックをほとんど使っていなくて、Rubyをまだまだ全然扱えていないなとメタプログラミングを呼んでいて感じたので、ブロックについての基本をまとめていきたいと思います。
ブロックの基本
まずはブロックの基本構文
def a_method(a, b)
a + yield(a, b)
end
a_method(1, 2) { |x, y| (x + y) * 3 } #=> 10
yeildがありながらブロックが渡されてこないとエラーになります。
def a_method(a, b)
a + yield(a, b)
end
a_method(1, 2)
block.rb:2:in `a_method': no block given (yield) (LocalJumpError)
メソッドの内部ではblock_given?メソッドブロックの有無を確認できる。
def a_method(a, b)
a + yield(a, b) if block_given?
end
a_method(1, 2) #=> nil
ブロックはクロージャ
ブロックはクロージャである。つまり、ブロックの中にローカル変数を束縛してメソッドに渡すことができる。
def my_method
x = 'Goodbye'
yield('cruel')
end
x = 'Hello'
my_method { |y| "#{x}, #{y} world" } #=> "Hello, cruel world"
メソッドの中にも束縛はあるが、ブロックからはメソッドの束縛は見えない。
スコープ
スコープの変化を追いかけてみる。
v1 = 1
class MyClass
v2 = 2
local_variables #=> [:v2]
def my_method
v3 = 3
local_variables
end
local_variables #=> [:v2]
end
obj = MyClass.new
obj.my_method #=> [:v3]
obj.my_method #=> [:v3]
local_variables #=> [:v1, :obj]
クラスを定義するときにクラス内のlocal_variablesが実行され、[:v2]を返していることが分かる。クラス内部からはトップレベルで定義されているv1は見えない。また、当然ながらクラス定義のときにはメソッド内部のlocal_variablesメソッドは実行されない。
続いて、MyClassクラスのインスタンスを作り、my_methodメソッドを呼び出すと[:v3]が変える。このメソッド内部からはメソッド外で定義しているローカル変数は見えない。
最後にトップレベルでのlocal_variablesメソッドの結果は[:v1, :obj]になっていて、クラス内部、メソッド内部のローカル変数は見えなくなっている。
スコープゲート
上記のように、Rubyプログラムがスコープを切り替えて新しいスコープをオープンする場所は3箇所ある。
- クラス定義
- モジュール定義
- メソッド
これらはスコープゲートとして振る舞う。
スコープゲートを超えて束縛を渡すにはどうすればいいか
v1 = 1
MyClass = Class.new do
v2 = 2
local_variables #=> [:v2, :v1, :obj]
define_method :my_method do
v3 = 3
local_variables
end
end
obj = MyClass.new
obj.my_method #=> [:v3, :v2, :v1, :obj]
local_variables #=> [:v1, :obj]
クラスの定義をclassキーワードの代わりにClassクラスのメソッド呼び出しに変えて、ブロックの中に変数を包み込めばそのメソッドに他のスコープの変数を渡すことができる。メソッド定義でも同様にdefキーワードの代わりにdefine_methodメソッドを使えば、他のスコープの変数を同様にクロージャに包んで渡すことができる。
このようにスコープゲートをメソッド呼び出しに置き換えると、他のスコープの変数が見えるようになる。これをスコープのフラット化(入れ子構造のレキシカルスコープ)という。
当然ながら外のスコープから中のレキシカルスコープを覗くことはできない。
スコープの共有の例
def define_methods
shared = 0
Kernel.send :define_method, :counter do
shared
end
Kernel.send :define_method, :inc do |x|
shared += x
end
end
counter #=> 0
inc(4) #=> 4
counter #=> 4
Kernel#counterと#incはローカル変数sharedを共有できている。
instance_eval
val = 'val'
class MyClass
def initialize
@v = 1
end
end
obj = MyClass.new
obj.instance_eval do
p self #=> #<MyClass:0x00007faabd8a8008 @v=1>
p @v #=> 1
p val #=> "val"
end
instance_evalに渡したブロックはレシーバをselfにしてから評価されるので、レシーバのインスタンス変数やprivateメソッドにアクセスできる。また、他のブロックと同じように、instance_evalを定義したときの束縛も見える。
さいごに
まだまだ実務で使えるレベルに理解が至ってませんが、引き続き学習を続けていきいつかは活用したいです。
その知見をまたアウトプットできればと思います。