Ruby

【メタプログラミング】ブロックについてあれこれ

実務でブロックをほとんど使っていなくて、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を定義したときの束縛も見える。

さいごに

まだまだ実務で使えるレベルに理解が至ってませんが、引き続き学習を続けていきいつかは活用したいです。
その知見をまたアウトプットできればと思います。

ABOUT ME
sakai
東京在住の30歳。元々は車部品メーカーで働いていてましたが、プログラミングに興味を持ちスクールに通ってエンジニアになりました。 そこからベンチャー → メガベンチャー → 個人事業主になりました。 最近は生成 AI 関連の業務を中心にやっています。 ヒカルチャンネル(Youtube)とワンピースが大好きです!