Railsガイドを読む(定数の自動読み込みと再読み込み)

More than 1 year has passed since last update.

定数の自動読み込みと再読み込みを読んでいて、よくわからなかったことを調べてみました。この章はRubyとRailsの定数に関する挙動を説明していますが、今回はRubyの挙動について調べます。

$ ruby -v
ruby 2.2.0p0 (2014-12-25 revision 49005)

です。

はじめに

最近こそこそとRailsガイドの翻訳をお手伝いしています。自分自身で翻訳することはほとんどなくて、主に出来上がった翻訳のチェックをしています。この章は内容が難しいわりに例となるコードが少ないため、(翻訳が適切でも)意味がとりにくい箇所があります。そこで、コードを書きながら補足説明をしようと思い、まとめました。

2 定数更新

定数更新を読む上でのポイントは

  1. ネスト(Module.nestingの結果)は名前空間と一致しないことがある
  2. 相対定数(A)と修飾済み定数(A::B)の違いは大きい
  3. ブロックは特別
  4. トップレベルも変わり者

です。

2.1 ネスト

Ruby内のある場所のネストを調べるにはModule.nestingを使用します

とのことなので、Module.nestingを使って、ネストを確認していきます。
ちなみに[A::B, X::Y]のところはRailsのmasterでは修正されていて、

module X
  module Y
  end
end

module A
  module B
  end
end

module X::Y
  module A::B
    # (3)
    # 例えばここで p Module.nestingすると[A::B, X::Y]が表示される
  end
end

が正しいそうです。

ここではmodule A::Bが修飾済み定数か相対定数かでネストは変わります。

module X
  module Y
  end
end

module A
  module B
  end
end

module X::Y
  module C
    p Module.nesting
    #=> [X::Y::C, X::Y]
  end
end

module X::Y
  module A::D
    p Module.nesting
    #=> [A::D, X::Y]
  end
end

Cのように相対定数のときはcrefに属しているのに対して、A::Dのように修飾済み定数のときは独立しているものとして扱われます。X::Y::A::Dではないんですよね。

特異クラスはclass << objectでオープンされるときにスタックにプッシュされ、後でスタックからポップされる。

class A
  class << self
    p Module.nesting
    #=> [#<Class:A>, A]
  end
  p Module.nesting
  #=> [A]
end

確かに特異クラスがプッシュされていますね。

*_evalで終わるメソッドが文字列を1つ引数に取って呼び出されると、そのレシーバの特異クラスはevalされたコードのネストにプッシュされる。

class A
end

A.class_eval("p Module.nesting")
#=> [A]
A.module_eval("p Module.nesting")
#=> [A]
A.instance_eval("p Module.nesting")
#=> [#<Class:A>]
A.new.instance_eval("p Module.nesting")
#=> [#<Class:#<A:0x007f94dc0c7150>>]

あれ、・・・

class A
end

A.class_eval("p Module.nesting.first.singleton_class?")
#=> false
A.module_eval("p Module.nesting.first.singleton_class?")
#=> false
A.instance_eval("p Module.nesting.first.singleton_class?")
#=> true
A.new.instance_eval("p Module.nesting.first.singleton_class?")
#=> true

class_evalmodule_evalのときはレシーバをプッシュしているようですね(Railsのmasterでは修正されています)。
ちなみにブロックを渡したときはネストにプッシュされません。

class A
end

A.class_eval { p Module.nesting }
#=> []
A.module_eval { p Module.nesting }
#=> []
A.instance_eval { p Module.nesting }
#=> []
A.new.instance_eval { p Module.nesting }
#=> []

そのためブロック内で定数を定義しようとすると、トップレベルに定義されたりします。

class A
end

A.class_eval("FOO = 10")

class B
end

B.class_eval { BAR = 20 }

p A::FOO
#=> 10
p B::BAR
#=> const.rb:12: warning: toplevel constant BAR referenced by B::BAR
#=> 20

Kernel#loadによって解釈されるコードのトップレベルにあるネストは空になる。ただしload呼び出しが第2引数としてtrueという値を受け取る場合を除く。この値が指定されると、無名モジュールが新たに作成されてRubyによってスタックにプッシュされる。

# const2.rb
module A
  p Module.nesting
end
p Module.nesting
load "const2.rb"
#=> [A]
#=> []
load "const2.rb", true
#=> [#<Module:0x007f8054137ac8>::A, #<Module:0x007f8054137ac8>]
#=> [#<Module:0x007f8054137ac8>]

たしかに無名モジュールが追加されていますね。

ブロックがスタックに何の影響も与えないという事実です。特に、Class.newやModule.newに渡される可能性のあるブロックは、newメソッドによって定義されるクラスやモジュールをネストにプッシュしません。

A = Class.new do
  p Module.nesting 
end
#=> []

これもたしかにプッシュしていません。

2.2 クラスやモジュールの定義とは定数への代入のこと

RubyはObjectにCという定数を作成し、その定数にクラスオブジェクトを保存します。

トップレベルで定義した定数はObjectの子として扱われます。詳しくはこちらもしくはこちらをみてください。

無名クラスや無名モジュールにひとたび名前が与えられた後は、定数とインスタンスで何が起きるかは問題ではありません。たとえば、定数を削除することもできますし、クラスオブジェクトを別の定数に代入したりどの定数にも保存しないでおくこともできます。名前はいったん設定された後は変化しなくなります。

まずは定数を削除してみます。削除してもKlass.nameは変わりません。

klass = Class.new
p klass.name
#=> nil

Klass = klass
p klass.name
#=> "Klass"
p Klass
#=> Klass

Object.send(:remove_const, :Klass)
p klass.name
#=> "Klass"
p Klass
#=> const.rb:10:in `<main>': uninitialized constant Klass (NameError)

次に他の定数にも代入してみます。p Klass2Klassと表示されます。へぇー。

klass = Class.new
Klass = klass
Klass2 = klass

p Klass.name
#=> "Klass"
p Klass2.name
#=> "Klass"
p Klass
#=> Klass
p Klass2
#=> Klass

2.4 解決アルゴリズム

3. 見つからない場合、crefに対してconst_missingが呼び出される。const_missingのデフォルトの実装はNameErrorを発生するが、これはオーバーライド可能。

module A
  def self.const_missing(name)
    p "const_missing of A"
    super
  end
  module B
    p Module.nesting
    #=> [A::B, A]
    def self.const_missing(name)
      p "const_missing of B"
      super
    end

    C
    #=> "const_missing of B"
    #=> const.rb:10:in `const_missing': uninitialized constant A::B::C (NameError)
  end
end

Cを呼び出した場所でのcrefはA::Bなので、たしかにBconst_missingが呼ばれています。

2. 探索の結果何も見つからない場合、親のconst_missingが呼び出される。const_missingのデフォルトの実装はNameErrorを発生するが、これはオーバーライド可能。

特に、ネストが何の影響も与えていない点にご注意ください。また、モジュールは特別扱いされておらず、モジュール自身またはモジュールの先祖のどちらにも定数がない場合にはObjectはチェックされない点にもご注意ください。

E = "e"

module X
  E = "xe"
end

module C
  # E = "ce"
  def self.const_missing(name)
    p "const_missing of C"
    super
  end
end

module X
  p Module.nesting
  #=> [X]
  p E
  #=> "xe"
  p C::E
  #=> "const_missing of C"
  #=> const.rb:27:in `const_missing': uninitialized constant C::E (NameError)
end

Eは相対定数なのでネストを順に探索し、X::Eを見つけています。一方でC::Eは修飾済み定数なのでCもしくはCの継承チェーンしか探索しません。そのため定数を発見できず、C(親)のconst_missingが呼ばれています。

おわりに

class_evalにブロックを渡したときと文字列を渡したときで、どうしてネストが変わるんでしょう。どちらのケースもdefをするとインスタンスメソッドが定義できる(つまりどちらもクラスのコンテキストにいる)のに。

class A
end

A.class_eval("def test; p 'test'; end")
A.class_eval do
  def test2
    p "test2"
  end
end

A.new.test
#=> "test"
A.new.test2
#=> "test2"

rubyのコードを読んでみるとNODE_FL_CREF_PUSHED_BY_EVALというフラグが影響しているようです。

Module.nestingではNODE_FL_CREF_PUSHED_BY_EVALフラグのたっているcrefを無視しています。

# eval.c
static VALUE
rb_mod_nesting(void)
{
    VALUE ary = rb_ary_new();
    const NODE *cref = rb_vm_cref();

    while (cref && cref->nd_next) {
    VALUE klass = cref->nd_clss;
    if (!(cref->flags & NODE_FL_CREF_PUSHED_BY_EVAL) &&
        !NIL_P(klass)) {
        rb_ary_push(ary, klass);
    }
    cref = cref->nd_next;
    }
    return ary;
}

ブロックを渡したclass_evalではpushしたcrefのNODE_FL_CREF_PUSHED_BY_EVALフラグを必ずたてるのに対して、文字列のときはSPECIAL_CONSTの時以外はNODE_FL_CREF_PUSHED_BY_EVALフラグをたてません。

#vm_eval.c
/* block eval under the class/module context */

static VALUE
yield_under(VALUE under, VALUE self, VALUE values)
{
    rb_thread_t *th = GET_THREAD();
    rb_block_t block, *blockptr;
    NODE *cref;

    if ((blockptr = VM_CF_BLOCK_PTR(th->cfp)) != 0) {
    block = *blockptr;
    block.self = self;
    VM_CF_LEP(th->cfp)[0] = VM_ENVVAL_BLOCK_PTR(&block);
    }
    cref = vm_cref_push(th, under, NOEX_PUBLIC, blockptr);
    cref->flags |= NODE_FL_CREF_PUSHED_BY_EVAL;

    if (values == Qundef) {
    return vm_yield_with_cref(th, 1, &self, cref);
    }
    else {
    return vm_yield_with_cref(th, RARRAY_LENINT(values), RARRAY_CONST_PTR(values), cref);
    }
}

/* string eval under the class/module context */
static VALUE
eval_under(VALUE under, VALUE self, VALUE src, VALUE file, int line)
{
    NODE *cref = vm_cref_push(GET_THREAD(), under, NOEX_PUBLIC, NULL);

    if (SPECIAL_CONST_P(self) && !NIL_P(under)) {
    cref->flags |= NODE_FL_CREF_PUSHED_BY_EVAL;
    }
    SafeStringValue(src);

    return eval_string_with_cref(self, src, Qnil, cref, file, line);
}

メソッド定義用のコンテキストと定数定義用のコンテキストを同じcrefで管理しながら、NODE_FL_CREF_PUSHED_BY_EVALフラグで調整しているのでしょうか。

あ、_evalファミリの修正PRがマージされました:D

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.