🕊

Rubyの定数探索について理解したい

2023/03/16に公開

挨拶

ポートでバックエンドを担当している新卒の @yuki.hara です。
今回は Ruby の定数探索がどのように行われているのか気になったため、調査しました。

基本的な定数探索の仕組み

まず、Ruby の定数探索には下記の 2 つのアルゴリズムが存在します。

  • 継承チェーンを利用した探索
  • レキシカルスコープチェーンを利用した探索

具体的にどのように探索されているか今から見ていこうと思います。

継承チェーンを利用した探索

継承チェーンとは、クラスの継承関係のことであり、メソッドの探索の時も同様に継承チェーンは利用されています。
ちなみに継承チェーン上に存在するクラスは、ancestorsメソッドで参照することができます。

定数探索時の動きとしては下記になります。

  1. クラス内に参照しにいく
  2. 参照するものが見つからない場合は、、親クラスに参照しにいく。
  3. 参照するものが見つかるまで、上記 1,2 を繰り返して、継承チェーンを辿っていく。

コードの具体例

class ParentClass
  CONSTANT = 'constant in ParentClass'
end

class ChildClass < ParentClass
  p CONSTANT
end

# 下記が出力結果
# "constant in ParentClass"

継承チェーンは下記のようになっています。

p ChildClass.ancestors #=> [ChildClass, ParentClass, Object, Kernel, BasicObject]

レキシカルスコープチェーンを利用した探索

レキシカルスコープは静的スコープとも呼ばれており、クラスや関数の定義時にスコープが決まります。

レキシカルスコープチェーンを理解するには下記の 2 つのをまず押さえる必要がある。

  • nd_next
  • nd_class

下記が図に表したものになっています。

https://patshaughnessy.net/2013/6/4/coming-this-fall-ruby-under-a-microscope-updated-and-in-print

nd_next とは?

nd_nextは、親のレキシカルスコープを保持しているポインタです。通常のクラスを作成した場合は、トップレベルのスコープ(Object クラスのレキシカルスコープ)が保持されます。

nd_class とは?

nd_classは、スコープに対応するクラスやモジュールの情報を保持しているポインタです。

定数探索時の動きとしては下記になります。

  1. nd_classを利用してスコープに対応するクラス or モジュールを参照する。
  2. クラス or モジュールが保持する定数テーブルに参照したい定数があるか確認する。
  3. 参照するものが見つからない場合は、nd_nextポインタを利用して親のレキシカルスコープに参照しにいく。
  4. 参照するものが見つかるまで、上記 1,2,3 を繰り返して、レキシカルスコープチェーンを辿っていく。

親のレキシカルスコープはnd_nextに保持しているが、self のレキシカルスコープは Ruby 内部のcrefにセットされています。
定数の自動読み込みと再読み込み (Classic) - Rails ガイド

コードの具体例

module NameSpace
  CONSTANT = 'constant in NameSpace'

  class ParentClass
    p CONSTANT
  end
end

# 下記が出力結果
# "constant in NameSpace"

レキシルスコープチェーンと継承チェーンはどちらが優先される?

上記で2通りの探索方法を説明してきたが、下記のような場合はどうなるでしょうか?

class ParentClass
  CONSTANT = 'constant in ParentClass'
end


module NameSpace
  CONSTANT = 'constant in NameSpace'

  class ChildClass < ParentClass
    p CONSTANT
  end
end

# 出力結果
# "constant in NameSpace"

出力結果から、レキシカルスコープの方が優先されると言うのがわかる。

ruby の基本的な定数探索のアルゴリズムは説明したのでもう少し深ぼっていく。

ruby には定数の参照方法が 3 つのパターンがある。

  • 絶対参照(::MainClass)
  • 相対参照(MainClass)
  • 修飾つき参照(MainClass::CONSTANT)

絶対参照とは?

絶対参照(::MainClass)についてはリファレンスに下記のように記載されていました。

Object クラスで定義されている定数(トップレベルの定数と言う)を確実に参照するためには左辺無しの`::'演算子が使えます。

従ってトップレベルの定数が参照できます。

定数

相対参照とは?

相対参照は前半部分で説明した定数探索が行われます。

修飾つき参照

定数を参照するときは基本的にこの方法が使用されます。

修飾つき参照は、下記の流れで処理されます。

  1. ::の左側にある定数が cref にセットされる。
  2. 継承チェーンのみを辿る。ただし、トップレベルとその祖先は探索しない。

トップレベルとその祖先は探索しない理由

https://bugs.ruby-lang.org/issues/11547

トップレベルとその祖先のクラスは探索しない理由は上記に記載してありました。

class Auth; end
class Twitter; end

上記のようなコードがあり、Twitter::Authで定数を参照しにいく場合にトップレベルとその祖先のクラスを探索できるとを探索できると次のような問題が起こる。

  1. 定数Twitterを相対参照する。
  2. ::Authを修飾つき参照すると、継承チェーンをたどり、Objectクラス配下にAuthクラスが発見されるので、本来Twitterクラスの中に定数はないので何も出力されないはずなのに出力されてしまう。

上記の理由から Ruby2.5 からトップレベルとその祖先のクラスは探索できないようになっている。

今回参照させていただいた記事・書籍

まとめ

最後まで見ていただきありがとうございます:bow:
今度はこれを踏まえて Rails が Model や Controller をどのように読み込んでいるか調査してみようと思います!

Discussion