ブロックとスコープを使いこなしたい

はじめに

この記事では「ブロックって何?」という方を対象にしております。
ブロックや束縛という概念についてご説明し、コードと束縛を自由に組み合わせられるinstance_evalメソッドをご紹介します。
この記事を通して、「instance_evalってやつすげー!」と思ってくれたら幸いです。
ブロックとスコープの知識を習得し、メタプログラミングの世界に足を踏み入れてみましょう!

実行環境

この記事は以下の動作環境で動作確認しています。

  • ruby (2.6.1)

ブロックとは

Rubyでは、do ~ end、または{ ~ }の処理の塊のことをブロックと言います。

一行で記述されるブロックは{ ~ }を使い、複数行のブロックにはdo ~ endを使う慣習があります。

ブロックを定義できるのは、メソッドを呼び出す時だけです。
ブロックはメソッドに渡され、メソッド内でyieldキーワードを使ってブロックをコールバックできます。

def my_method(a, b)
  a + yield(a, b)
end

p my_method(1, 2){ |x, y| x + y } # => 4

ブロックの基本的な説明はここまでにして、早速内容に入っていきましょう。

ブロックと束縛

ブロックを呼び出すためには、ローカル変数、インスタンス変数、selfといった環境が必要になります。
このオブジェクトに紐づけられている環境の事を束縛とも言います。

つまり、ブロックとはコードと束縛両方の集まりという事になります。

ブロックにおいて、コードはブロック内にあるコードを使用し、束縛はブロックを定義した場所にある束縛を使用します。

そして、ブロックをメソッドに渡した時は、その束縛も一緒に運ばれていきます。
メソッドにある束縛はブロックからは見る事が出来ません。

def my_method
  my_var = "my_methodの変数"
  yield
end

my_var = "トップレベルの変数"
p my_method { my_var } # => "トップレベルの変数"

ブロックと束縛を理解するためには、スコープに関する知識を深める必要があります。

スコープゲート

スコープゲートとは、スコープが変化する境界線の事です。(スコープに関しての基本的な説明は省きます)
Rubyでは、下記の3つがスコープゲートになります。

  • クラス定義
  • モジュール定義
  • メソッド

実際に下記のコードを見ながら確認していきましょう。

top_level_var = "トップレベルの変数"
local_variables # => [:top_level_var, :obj]

class MyClass #クラス定義
  my_class_var = "マイクラスの変数"
  local_variables # => [:my_class_var]

  def my_method # メソッド定義
    my_method_var = "マイメソッドの変数"
    local_variables # => [:my_method_var]
  end

  local_variables # => [:my_class_var]
end

local_variables # => [:top_level_var, :obj]

このような感じですね。

それぞれのスコープ内で束縛が変わっている事が確認できると思います。
思わぬ所で、予期せぬ変数が参照されるエラーが発生し難くなり便利ですね。

しかし、Rubyを使っていると、スコープゲートを超えて束縛を渡したい局面に遭遇する事があります。

そういう時はフラットスコープを使っていきましょう。

スコープゲートを超えて束縛を渡す(フラットスコープ)

クラス定義ではなく、メソッド呼び出しを使用する事でスコープゲートを超えることが可能になります。
(正確には、スコープゲートを超えているわけではなく、スコープゲートを使用していないだけです)

先程のscope_gate.rbを書き換えていきましょう。
もしMyClass内でtop_level_varを使いたい時はこのように書きます。

top_level_var = "トップレベルの変数"
local_variables # => [:top_level_var, :obj]

MyClass = Class.new do #クラス定義
  my_class_var = "マイクラスの変数"
  local_variables # => [:my_class_var, :top_level_var, :obj]

  def my_method # メソッド定義
    my_method_var = "マイメソッドの変数"
    local_variables # => [:my_method_var]

  end
  local_variables # => [:my_class_var, :top_level_var, :obj]
end

local_variables # => [:top_level_var, :obj]

my_method内でmy_class_varを使いたい場合も、先程と同じように、メソッド呼び出しに書き換えます。

top_level_var = "トップレベルの変数"

class MyClass #クラス定義
  my_class_var = "マイクラスの変数"
  local_variables # => [:my_class_var]

  define_method :my_method do # メソッド定義
    my_method_var = "マイメソッドの変数"
    local_variables # => [:my_method_var, :my_class_var]

  end
  local_variables # => [:my_class_var]
end

local_variables # => [:top_level_var, :obj]

技術的には、この方法の事を入れ子構造のレキシカルスコープと呼びます。
しかしRubyに限ってはフラットスコープと呼ばれる事が多いようです。

フラットスコープを応用すれば、自由自在にスコープを組み合わせる事ができます。
共有スコープという方法もありますが、上記の2つ(スコープゲート、フラットスコープ)を理解していれば自然と使えるようになる方法なので省きます。(気になる方は調べてみてください)

コードと束縛を好きなように組み合わせる(instance_eval)

instance_evalというメソッドがあります。

instance_evalに渡したブロックは、オブジェクトのコンテキストで評価されます。
つまり、レシーバがselfにしてから評価されるので、レシーバのprivateメソッドやインスタンス変数などにもアクセスが可能になるという事です。

また、他のブロックと同じように、instance_evalを定義した時の束縛も見ることが出来ます。

それでは、コードを見ていきましょう。

top_level_var = "トップレベルの変数"

class MyClass
  def initialize
    @my_instance_var = "マイクラスのインスタンス変数"
  end
end

my_class = MyClass.new

my_class.instance_eval do
  self # => #<MyClass:0x00007fc4b08bdcc0 @my_instance_var="マイクラスのインスタンス変数">
  top_level_var # => "トップレベルの変数"
  @my_instance_var # => "マイクラスのインスタンス変数"
end

このような感じです。

instance_evalに渡したブロックのことは、インスタンス探査機と呼ばれます。
オブジェクトの内部を探査して、そこで実行するコードだからです。

おわりに

ブロックや束縛という概念についてご説明し、コードと束縛を自由に組み合わせられるinstance_evalメソッドをご紹介いたしました。
これを気にメタプログラミングに興味を持っていただければ幸いです。