ようへいの日々精進XP

よかろうもん

Ruby の組み込みライブラリ (クラス) の「学習テスト」を書いて, 出来るだけ多くのメソッドと出会いたい (4) 〜 class_eval 定数参照が解らない ~

これは...

いつまで続くかわからないシリーズである.

inokara.hateblo.jp

今回はテストを書くかは置いといて, class_eval でメソッドを定義した時の定数参照 (定数参照のスコープ) についてドキュメントを読んでも, イマイチ理解出来ないので色々と調べたことを書きなぐっていく.

class_eval

Module#class_eval はドキュメントによると, 以下のように記載されている.

モジュールのコンテキストで文字列 expr またはモジュール自身をブロックパラメータとするブロックを 評価してその結果を返します。 モジュールのコンテキストで評価するとは、実行中そのモジュールが self になるということです。 つまり、そのモジュールの定義式の中にあるかのように実行されます。 ただし、ローカル変数は module_eval/class_eval の外側のスコープと共有します。 文字列が与えられた場合には、定数とクラス変数のスコープは自身のモジュール定義式内と同じスコープになります。 ブロックが与えられた場合には、定数とクラス変数のスコープはブロックの外側のスコープになります。

以下, class_eval の利用例. 文字列としてメソッドを与えていている.

irb(main):001:0> class Cls; end
=> nil
irb(main):002:0> Cls.class_eval %Q{ def foo; puts "aaaa"; end}
=> :foo
irb(main):003:0> Cls.new.foo
aaaa
=> nil

Cls クラス内で foo メソッドを定義するのと同義で, 普通にインスタンスメソッドとして呼び出すことが出来る. 以下も同じような感じ.

irb(main):001:0> class Cls; end
=> nil
irb(main):002:0> Cls.class_eval do
irb(main):003:1*   def foo
irb(main):004:2>     puts "aaaa"
irb(main):005:2>   end
irb(main):006:1> end
=> :foo
irb(main):007:0> Cls.new.foo
aaaa
=> nil

定数の参照について解らない

改めて class_eval は

class_eval は以下のようにメソッドの定義を文字列でも与えることが出来るのは先述の通りで, ブロックで与えることも可能である.

irb(main):001:0> class Cls1; end
=> nil
irb(main):002:0> Cls1.class_eval "def foo; puts 'class_eval dayo'; end"
=> :foo
irb(main):003:0> Cls1.new.foo
class_eval dayo
=> nil

メソッドの定義方法によって変わる定数のスコープ

で, 以下のようなコードがあった場合, 定数 CONST 参照の可否がメソッドの定義方法によって変わるという挙動の理由がイマイチぴんと来ていない.

# パターン 1
class Cls
  CONST = 'class_eval dayo'
end

module Mod
  Cls.class_eval "def foo; CONST; end"
end

# パターン 2
class Cls
  CONST = 'class_eval dayo'
end

module Mod
  Cls.class_eval do
    def foo
      CONST
    end
  end
end

パターン 1 とパターン 2 をそれぞれ, irb で実行してみる.

# パターン 1 メソッドを文字列として与えている
irb(main):001:0> class Cls
irb(main):002:1>   CONST = 'class_eval dayo'
irb(main):003:1> end
=> "class_eval dayo"
irb(main):004:0> 
irb(main):005:0* module Mod
irb(main):006:1>   Cls.class_eval "def foo; CONST; end"
irb(main):007:1> end
=> :foo
irb(main):008:0> Cls.new.foo
=> "class_eval dayo"

# パターン 2 ブロックでメソッドを与えている
irb(main):001:0> class Cls
irb(main):002:1>   CONST = 'class_eval dayo'
irb(main):003:1> end
=> "class_eval dayo"
irb(main):004:0> 
irb(main):005:0* module Mod
irb(main):006:1>   Cls.class_eval do
irb(main):007:2*     def foo
irb(main):008:3>       CONST
irb(main):009:3>     end
irb(main):010:2>   end
irb(main):011:1> end
=> :foo
irb(main):012:0> Cls.new.foo
NameError: uninitialized constant Mod::CONST

以下のような違いを確認している.

  • メソッドを文字列で与えた場合には, Cls 内で定義している定数を参照出来ている
  • ブロックでメソッドを与えた場合には, Mod 内の定数を参照しようとして NameError 例外となっている

そんなものなのか...で終わらすのもアレなので, 改めて, ドキュメントの一部を引用.

文字列が与えられた場合には、定数とクラス変数のスコープは自身のモジュール定義式内と同じスコープになります。 ブロックが与えられた場合には、定数とクラス変数のスコープはブロックの外側のスコープになります。

んー, 自分の読解力の無さを恨みたいけど, 以下のような解釈で良いのかな...(自信無い

  • メソッドをブロックで定義した場合には, class_eval ブロックの外側, 上記の例だと, module 内で定数, クラス変数を探索する
  • メソッドを文字列で定義した場合には, class_eval で定義するメソッドを定義するクラス内の定数, クラス変数を探索する

んんんー, 解らないというか, なんだろうこのモヤモヤは...

特に, 以下の部分が解らない...

定数とクラス変数のスコープは自身のモジュール定義式内と同じスコープになります。

一応, テストも書く

以下のようなテストを書いた.

# file name: 12.rb
require 'minitest/autorun'
require "minitest/reporters"
Minitest::Reporters.use! [Minitest::Reporters::SpecReporter.new]

class Cls
  CONST = 'class_eval dayo'
end

module Mod
  Cls.class_eval "def foo; CONST; end"
end

module Mod
  Cls.class_eval do
    def bar
      CONST
    end
  end
end

class GakushuTest < Minitest::Test
  def test_const_reference_1
    assert_equal Cls.new.foo, 'class_eval dayo'
  end

  def test_const_reference_2
    assert_raises NameError do
      Cls.new.bar
    end
  end
end

Cls.new.foo は文字列で Cls クラスにメソッドを定義している為, 定数は参照出来るので, 定数の内容が返ってくることを期待する. Cls.new.bar はブロックで定義されたメソッドを呼び出していて, 定数のスコープは Cls クラスを外れる為, NameError 例外が発生することを期待している.

テストを実行すると, 以下のように出力される.

$ bundle exec ruby 12.rb 
Started with run options --seed 32749

GakushuTest
  test_const_reference_2                                          PASS (0.00s)
  test_const_reference_1                                          PASS (0.00s)

Finished in 0.00102s
2 tests, 2 assertions, 0 failures, 0 errors, 0 skips

一応, いい感じ.

以上

まだモヤモヤが治まらないけど, メモでした.