Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyのバインディングとレキシカルスコープを徹底解説

あけましておめでとうございます。Ruby Magicへようこそ!今回の冬のエピソードでは、バインディングスコープについて深く掘り下げます。それでは、スキーを履いて森の奥深くまでご一緒しましょう。

前回は、ブロック・Proc・ラムダを比較しながらRubyのクロージャについて学びました。3つの違いに触れるとともに、「クロージャとは何か」についても簡単に説明しました。

クロージャとは、環境(environment)を持つ第一級関数です。環境とは、クロージャが作成された時点で存在していた変数へのマッピングのこと。クロージャは、たとえその変数が別のスコープで定義されていても、アクセス権を保持し続けます。

前回はRubyにおける第一級関数に相当するものを探りましたが、環境については都合よく省略していました。今回は、クロージャやクラス、クラスインスタンスにおいてこの環境がどのように機能するのかを、Rubyがバインディングを通じてレキシカルスコープをどのように扱うのかを見ながら解説します。

レキシカルスコープとは

プログラミングにおいて、スコープとはコードの特定の部分で利用可能なバインディング(名前束縛)の集合を指します。バインディングとは、変数名とその値のように、名前とメモリ参照を結びつけるものです。スコープは、selfが何を指すのか、どのメソッドを呼び出せるのか、どの変数が利用できるのかを決定します。

Rubyは、多くの現代的なプログラミング言語と同様に、静的スコープを採用しています。これは一般にレキシカルスコープ(字句スコープ)と呼ばれ、動的スコープ(dynamic scope)とは対照的な概念です。現在のスコープはコードの構造に基づいて決まり、コードの特定の部分で利用できる変数を決定します。つまり、メソッド、ブロック、クラス間を行き来するとスコープは変化します。それぞれが異なるローカル変数を持てるからです。

def bar
  foo = 1
  foo
end
 
bar #  => 1

このメソッドでは、メソッド内でローカル変数を作成し、それを返しています。変数はメソッド内で作成されているため、メソッド内ではスコープ内(in scope)にあります。

foo = 1
 
def bar
  foo
end
 
bar # => NameError (undefined local variable or method `foo' for main:Object)

一方、この例では変数をメソッドの外側で作成しています。メソッド内でその変数を呼び出すとエラーになります。変数がスコープ外(out of scope)だからです。ローカル変数は厳密にスコープされるため、引数として渡されない限り、メソッドは自分の外側にある変数にアクセスできません。

@foo = 1
 
def bar
  @foo
end
 
bar #  => 1

ローカル変数がローカルでのみ利用可能であるのに対し、インスタンス変数はクラスインスタンスのすべてのメソッドから利用できます。

継承されたスコープとProcのバインディング

これまでの例で見てきたように、スコープはコード上の位置によって決まります。メソッドの外側で定義されたローカル変数はメソッド内ではスコープ外ですが、インスタンス変数にすることで利用可能になります。メソッドが外側のローカル変数にアクセスできないのは、メソッドが独自のスコープと独自のバインディングを持っているためです。

しかし、Proc(ブロックやラムダも含みます)は事情が異なります。Procが生成されるとき、そのブロックが作成されたコンテキストにあるローカル変数への参照を継承したバインディングが作られます。

foo = 1
Proc.new { foo }.call # => 1

この例では、fooという変数に1を設定しています。内部的には、2行目で作成されたProcオブジェクトが新しいバインディングを生成します。procを呼び出すと、変数の値を取得できます。

バインディングはprocの初期化時に作成されるため、変数を定義する前にprocを作成することはできません。たとえブロックの呼び出しが変数定義後に行われるとしてもです。

proc = Proc.new { foo }
foo = 1
proc.call # => NameError (undefined local variable or method `foo' for main:Object)

procを呼び出すとNameErrorが発生します。変数がprocのバインディング内で定義されていないからです。したがって、proc内でアクセスする変数は、procの作成前に定義するか、引数として渡す必要があります。

foo = 1
proc = Proc.new { foo }
foo = 2
proc.call # => 2

ただし、メインのコンテキストで変数を定義した後に変更することは可能です。procのバインディングは変数をコピーせず、参照を保持しているからです。

foo = 1
Proc.new { foo = 2 }.call
foo #=> 2

この例から、foo変数がprocの内外で同じオブジェクトを指していることがわかります。procの中で更新すると、外側の変数も一緒に更新されます。

バインディング

Rubyは現在のスコープを追跡するためにバインディングを使用します。バインディングは、コードの各位置における実行コンテキストをカプセル化したものです。bindingメソッドは、現在位置のバインディングを記述するBindingオブジェクトを返します。

foo = 1
binding.local_variables # => [:foo]

Bindingオブジェクトには#local_variablesというメソッドがあり、現在のスコープで利用可能なすべてのローカル変数の名前を返します。

foo = 1
binding.eval("foo") # => 1

#evalメソッドを使えば、バインディング上でコードを評価できます。上の例はあまり実用的ではありません。単にfooを呼び出しても同じ結果になるからです。しかし、バインディングは受け渡し可能なオブジェクトなので、もっと面白い使い方ができます。例を見てみましょう。

実践的な例

ガレージで安全にバインディングについて学んだところで、ゲレンデに出て雪遊びをしてみましょう。Ruby言語内部でのバインディングの使用以外にも、Bindingオブジェクトが明示的に使われる場面があります。良い例がERB(Rubyのテンプレートシステム)です。

require 'erb'
 
x = 1
 
def y
  2
end
 
template = ERB.new("x is <%= x %>, y() returns <%= y %>, self is `<%= self %>`")
template.result(binding) # => "x is 1, y() returns 2, self is `main`"

この例では、変数x、メソッドy、そして両方を参照するERBテンプレートを作成しています。そして現在のバインディングをERB#resultに渡します。これにより、テンプレート内のERBタグが評価され、変数が埋め込まれた文字列が返されます。

内部では、ERBはBinding#evalを使い、渡されたバインディングのスコープ内で各ERBタグの内容を評価します。上の例で動作する簡略化した実装は次のようになります。

class DiyErb
  def initialize(template)
    @template = template
  end
 
  def result(binding)
    @template.gsub(/<%=(.+?)%>/) do
      binding.eval($1)
    end
  end
end
 
x = 1
 
def y
  2
end
 
template = DiyErb.new("x is <%= x %>, y() returns <%= y %>, self is `<%= self %>`")
template.result(binding) # => "x is 1, y() returns 2, self is `main`"

DiyErbクラスは初期化時にテンプレート文字列を受け取ります。#resultメソッドはすべてのERBタグを見つけ、その内容を評価した結果で置き換えます。そのために、渡されたバインディングに対して、ERBタグの内容を引数にBinding#evalを呼び出します。

#resultメソッドを呼び出す際に現在のバインディングを渡すことで、evalの呼び出しは、メソッドの外側で、さらにはクラスの外側で定義された変数にも、明示的に渡すことなくアクセスできるのです。

まとめ:森で迷子になっていませんか?

スキー旅行をお楽しみいただけたでしょうか。今回は、これまで軽く流してきたスコープとクロージャについて、より深く掘り下げました。読者の皆さんが森で迷子になっていないことを願っています。バインディングについてさらに詳しく知りたい場合や、他に取り上げてほしいRubyのトピックがあれば、ぜひお知らせください。

最後までお付き合いいただきありがとうございました。次の開発者にバインディングを残す前に、雪を払い落としておきましょうね。こうした魔法のような旅が好きな方は、月に一度ほど公開される新しい記事の通知を受け取るために、Ruby Magicを購読してみてはいかがでしょうか。

  1. RuboCopで実現するRubyコードのリントと自動フォーマット徹底ガイド

    リンティング(Lint)とは、ソースコードに含まれるプログラム上の誤りやスタイル違反を自動的に検出する仕組みです。このチェックは「リンター」と呼ばれる静的コード解析ツールによって行われます。一方、コードフォーマッターは、あらかじめ設定されたルールセットにソースコードを厳密に準拠させることに特化したツールです。一般的にリンターは違反を報告するだけで、実際の修正はプログラマーの手に委ねられます。それに対してコードフォーマッターは、ルールを直接ソースコードに適用し、フォーマットのミスを自動的に修正してくれます。 プロジェクトで一貫性のあるコードスタイルを実現するには、通常リンティング用とフォーマッ

  2. Rubyでのログ出力をマスターする:LoggerとLogrageの使い方徹底解説

    Rubyでのログ出力入門:LoggerとLogrageの使い方 ロギングは、アプリケーション開発において最も重要なタスクの一つです。ログは以下のような場面で活用されます。 アプリ内部で何が起きているかを把握したいとき アプリケーションを監視したいとき 特定のデータに関するメトリクスを収集したいとき 新しいプログラミング言語を学ぶ際、最初に選ばれるのはその言語がネイティブに備えているロギング機構でしょう。標準機能は通常、扱いやすく、ドキュメントも充実しており、コミュニティでも広く使われています。 ただし、ログデータの内容や扱い方は、企業の方針、ビジネスの性質、アプリケーションの種類に