執筆: Misaki-i3 前回の記事でクラス・モジュールの継承チェーンを学び、メソッドの探索経路を攻略した私は、「これで定数も読めるようになった」と安心していた。それはもちろん勘違いであった。 本記事では、はじめに定数探索を習得する上で必要な観点をざっくりお伝えし、のちの章で個々を掘り下げていく構成とする。 この記事だけで定数探索のポイントを1から10まで習得できることを目指し、できるだけ多くの混乱しやすいポイントに対応できるよう心がけた。 それにしても謎なのが、各所の解答・解説に頻出する『定数の参照はレキシカルに決まるので答えは○○』という、何も明らかにならない説明だ。『レキシカルに決まる』という慣用句でもあるのか? 今でも得心は行っていない。
もし同じ思いをした方がご覧であれば、本記事ではこの言葉を用いずに説明しきるので、よくよく安心してほしい。 前回に引き続き、この記事は入門書を開いたばかりの初心者向きではないが、Rubyの入門書を一周した初心者には十分理解できるものと思う。 Ruby Goldの合格を目指している学習者には、ぜひ理解しておいてもらいたい内容である。定数参照がボーナス問題になると、かなり余裕ができるはずだ。 今回、メソッドの探索経路の詳細な知識は不要であるが、クラス・モジュールの継承チェーンの理解は必須となる。 今回はRubyの定数探索に限定した解説となる。 Rubyのフレームワークを利用した場合も同様で、例えばRuby on Railsには独自の定数探索経路が追加されているため、正しく参照先を特定するためにはRuby on Railsの理解が重要となる。 探索経路の解説をスムーズに理解していただくため、以下の2点を前提知識として確認されたい。 メソッド呼び出しをよく練習した人ほど、レシーバを記述していない呼び出しを見ると、反射的に頭の中で ここを勘違いしたままであると、次のコードにおいて、 これを見て「なぜ もし先のサンプルが、メソッド このときは、たしかに これは、レシーバの記述を省略して呼び出された この仕組みがメソッド呼び出しのポリモーフィックな挙動を実現している。 元のサンプルに戻ろう。 コメントのとおり、定数には暗黙のレシーバ ちなみに私は、「 メソッド呼び出しのドット( ちなみに…… 重要な前提を共有できたところで、いよいよ本題だ。 定数の探索経路は1種類ではない。また、呼び出し方によって探索経路を選択することができる。 定数の探索経路は、大きく2つある。 定数の参照の記述もまた、大きく2つに分けられる。 最後に、参照の記述と探索経路は、次のような関係になる。 この時点で、定数の探索は奇妙で易しくないということが分かったかもしれない。(特に米印のあたり) ネストの理解は十分であるだろうか。探索経路の基準となるものだ。 ネストを利用した定義の例を用意した。 このメソッドの返す配列の内容が、そのまま探索経路となる。 ネストの説明に使ったサンプルの各所で すべての結果に違和感がないかをよく確認してほしい。 頭の中で 先程述べた通り、この配列の並びがそのまま探索経路となる。 ここで生じるべき疑問は、「例えば 継承チェーンが絡まないことを確認するサンプルを用意した。 ネストを辿る探索経路に 上のサンプルをイメージ化すると、このような感じ。 ネストと これは文字通りの意味しかなく、継承チェーンがそのまま探索経路となっている。 継承チェーンのおさらい(メソッド探索の記事で使用した図) メソッドの探索と同様、この図の青色(superclass)の矢印を追うだけでよい。 概要の章では確か、場合によって継承チェーンをどこまで辿るかが異なるかのような説明を見かけたが……それはのちの章で扱う。 繰り返しになるが、定数の参照の記述は、大きく2つに分けられる。 様々な記述の例 つまり、今回の例では同じ Ruby のライブラリを始めとした既存のコードを読み解く機会には、このことを忘れないようにしたい。 さて、探索の種類と記述の種類を学び終えたら、あとは組み合わせるだけだ! 定数名のみを記述した場合の探索経路は次のようになる。 非常に喜ばしいことに、これ以上説明すべきことが何もない。 定数名のみで呼び出されている 答えは以下のようになる。 よって、 ダブルコロン演算子を用い記述した場合の探索経路は次のようになる。 書いてあるとおり、継承チェーンを辿る探索は ひとつ前のサンプルと同じ構造にしているので、 よって、 「定数名だけのときと、ダブルコロンを使うとき、 ダブルコロン演算子を用い記述する方の定数探索も、かつては 次のリンクは、ダブルコロンの記法を用いて指定オブジェクトからの探索を行うとき、トップレベルまで探索されてしまうことのデメリットを指摘するチケットである。 Feature #11547: remove top-level constant lookup - Ruby master - Ruby Issue Tracking System 要約すると、「呼び出そうとした定数が指定オブジェクトの配下になかったとき、Object まで探索されると意図しない定数を参照してしまいやすくなり、不便である」という内容である。 あえてオブジェクトを指定して定数を呼び出そうとするときは、トップレベルを探索する意図はないことの方が多い。その意図を持って利用するケースがあるとしても、そうでない多くの開発者の被るデメリットのほうが大きいと考えられる。 規則が一貫していることは重要だが、実用に配慮する柔軟性もかけがえのないものなのだろう。 ここまでで、定数の探索経路の解説は完了だ。 記事は続くが、想定より長くなってしまったのであとがきを繰り上げることにした。 私が定数の探索経路を学んでいるとき、見つけられる既存の解説では十分に理解できなかった。 この記事では何が何でも『一読で』『誤解なく』『分かってもらう』つもりで執筆しているが、残念ながら既に誤解を与えている可能性は低くないだろうと覚悟している。 記事中にあるサンプルを実行したり、「ここに定義を追加したらどんな影響があるだろう」とアレンジしてみたりしてほしい。 Ruby Goldの模擬問題を解き、模擬試験サイトの点数に勘違いを突きつけられ、また記事を読み返しに戻ってきてほしい。 弊社では採用活動を実施しています。 新卒採用:https://www.i3-systems.com/new-graduates/ 以降は、Ruby Goldの受験を目指す向けの追加情報と、定数探索を確認・練習するためのサンプルを記載している。 メタプログラミングの領域となるが、Ruby Gold の出題範囲であるため、資格取得を目指す人は押さえておくべきだ。 実際に定義した場合の例 定数探索の問題は、定数名のみを記述したときの探索がほとんどなので、それを前提にする。 (クラスと特異クラスに継承関係はないからね、ということ)
まえがき
模擬試験サイトの点数に勘違いを突きつけられると、私はさっさと書籍とWebを徘徊する仕事に戻った。
定数の挙動と私の解釈の辻褄が合うまでに、私は定数の探索がメソッドのそれよりも奇妙であることを確信できていた。
文量に見合う情報量となっているだろうから、お付き合いいただきたい。対象レベル
本記事では継承チェーンの解説は行わないため、自信のない方は前回の記事から必要な部分を補いながら読み進めてもらいたい。注意
他の言語にも当てはめられるものではないので注意していただきたい。準備運動 2つの前提知識
self.
)は行われないメソッド呼び出しのような、暗黙的なレシーバの補完(
self.
)は行われないself.
を補完してしまうかもしれない。
(そのような思い込みがない方は、本項を読み飛ばしていただいて問題ない)C2.new.const
が 'C2'
を返すような気がしてしまう。class C1
FOO = 'C1'
def const
FOO
end
end
class C2 < C1
FOO = 'C2'
end
C2.new.const # 'C1'
'C1'
を返すのか」が分からない場合は、まず「なぜ 'C2'
が返す気がしたか」を明確にしよう。なぜ
C2.new.const
が 'C2'
を返すような気がしたかfoo
のサンプルだったとすると……class C1
def foo { 'C1' }; end
def const
foo # self.foo と同じ
end
end
class C2 < C1
def foo { 'C2' }; end
end
C2.new.const # 'C2'
C2.new.const
は 'C2'
を返す。foo
が、暗黙的にカレントオブジェクトをレシーバとして補うためだ。(self.foo
と同等)
サンプルでは C2.new
がレシーバであるから、C2.new
のインスタンスメソッド foo
を実行する。定数呼び出しは暗黙の
self.
が補われないclass C1
FOO = 'C1'
def const
FOO # self.FOO ではない
end
end
class C2 < C1
FOO = 'C2'
end
C2.new.const # 'C1'
self.
が補われない。
そのためこのときの FOO
は、記述箇所である C1クラスの FOO
を見つけ、'C1'
を返す。FOO
は自分がインスタンスメソッド中にいることに気付いていない」とイメージしている。
FOO
から見た景色が次のようであると想像してみると、この FOO
が 'C1'
を返すことに違和感はないはずだ。class C1
FOO = 'C1'
FOO
end
ダブルコロンの記法
.
)と同じイメージで考えてよい。
呼び出す側::呼ばれる側
の関係で、チェーンもできる。
::
)を用いて、探索の始点となるオブジェクトを指定できる(Sample::CONST
)::CONST
)すると、左辺をトップレベルとみなす(Object::CONST
)Object::Sample::CONST
)
.
)の役割も包含している(エイリアスではない)Sample::class
, 'str'::reverse
).
)で定数は呼び出せない定数探索の概要
::
)演算子を用いてオブジェクトを指定し記述する
しかし理解が進めば、その奇妙さにも歴史的経緯があり、いたずらなものではないと気付ける。探索経路(1) クラス・モジュールのネストを辿る探索
クラス・モジュールのネストとは
module
キーワードや class
キーワードによる定義を入れ子(ネスト)にできる
ダブルコロンの記法の復習も兼ねて、自身の理解と異なる結果が生じていないかを確認してほしい。module M1 # M1
class C1 # M1::C1
class C2 # M1::C1::C2
end
end
end
class C1 # C1
end
class M1::C1 # M1::C1
class C2 # M1::C1::C2
end
class ::C1 # C1
end
class ::M1::C1 # M1::C1
end
end
M1::C1
と C1
は別物であることを押さえておくように。
後半は少々イレギュラーな書き方をしているが、難しく考えず、素直に読めばきっと大丈夫。Module.nesting
メソッドModule.nesting
は、クラス・モジュールのネストの状態を確認するためのメソッドだ。
このメソッドは、呼び出した箇所(記述箇所)のネストの状態を、クラス・モジュールの配列にして返してくれる。
現在のクラス→ひとつ外側のクラス→もうひとつ外側のクラス……と、内側から外側の順に並んでいる。Module.nesting
を呼んでみた。p Module.nesting # []
module M1
p Module.nesting # [M1]
class C1
p Module.nesting # [M1::C1, M1]
class C2
p Module.nesting # [M1::C1::C2, M1::C1, M1]
end
end
end
class C1
p Module.nesting # [C1]
end
class M1::C1
p Module.nesting # [M1::C1]
class C2
p Module.nesting # [M1::C1::C2, M1::C1]
end
class ::C1
p Module.nesting # [C1, M1::C1]
end
class ::M1::C1
p Module.nesting # [M1::C1, M1::C1]
end
end
違和感があればネストの説明に戻って、納得行くまで往復することを強く勧める。
特に、ふたつの M1::C1::C2
クラス内の Module.nesting
の違いに気付き、「確かにこの結果になる」と確信できること。Module.nesting
の返り値を求められるようになったなら完璧だ。ネストを辿る探索の経路 =
Module.nesting
Module.nesting
の返り値が [M1::C1::C2, M1::C1, M1]
である箇所なら、
探索経路は M1::C1::C2
→ M1::C1
→ M1
となる。M1::C1
を探す段になったとき、これの superclass の定数は見つかるのか?」というものだ。
見つからない、が答えである。ネストを辿る探索の最中に、継承チェーンを辿る探索は発生しない。[M1::C1::C2, M1::C1, M1]
なら、M1::C1::C2
→ M1::C1
→ M1
のみなのだ。class Parent
CONST = 'Parent'
end
class C1 < Parent
class C2
p Module.nesting # [C1::C2, C1]
# p CONST # NameError
end
end
C1
クラスが存在するが、そのスーパークラスである Parent
までは探しに行かないため、定数の呼び出しで NameError
が発生している。Module.nesting
だけ分かれば、ネストを辿る探索は攻略完了!
くれぐれも、深読みしすぎて探索経路を勝手に増やすことのないよう!探索経路(2) 継承チェーンを辿る探索
継承チェーンに関しては理解されているものとして進むので、新しく説明できることは実はない。
もちろん、水色(singleton_class)の矢印を通ることはない。クラスとその特異クラスは継承関係ではないためだ。定数の参照の記述
::
)演算子を用いて記述CONST = 'トップレベル'
class Sample
CONST = 'Sampleクラス'
puts CONST # Sampleクラス
puts ::CONST # トップレベル
puts ::Sample::CONST # Sampleクラス
end
puts CONST # トップレベル
puts Sample::CONST # Sampleクラス
puts ::Sample::CONST # Sampleクラス
puts CONST
で呼び出されているところが定数のみの記述だ。
他はダブルコロン演算子を用いた記述で、探索の始点となるオブジェクトを指定している。puts Sample::CONST
と puts ::Sample::CONST
の違いにも注目してみてほしい。
CONST
にとっては、どちらもダブルコロン演算子を用いた記述で、探索の始点は Sample
クラスである。
違いがあるのはダブルコロンの左辺だ。 Sample
か ::Sample
かという違いが存在している。ということは Sample
を見つけるまでの探索経路は異なる。Sample
を取得できているものの、場合によっては別のオブジェクトが取得されている可能性がある。参照の記述と探索経路(1) 定数名のみ記述・記述箇所からの探索
例題で確認してみよう。module M1
module M2
FOO
end
end
FOO
がある。この探索経路はどうなるだろう?
FOO
の箇所に Module.nesting
があったとき、返り値はなにか?
FOO
が記述されている M1::M2
の継承チェーンは?module M1
module M2
p Module.nesting # [M1::M2, M1]
FOO # (1) M1::M2 → M1 (2) M1::M2 → Object → Kernel → BasicObject
end
end
FOO
の探索経路は、
( M1::M2
→ M1
) → ( M1::M2
→ Object
→ Kernel
→ BasicObject
)
となる。Module.nesting
を攻略したあなたには、拍子抜けするほど簡単なのではないだろうか。
記事の最後にもいくつかのサンプルを用意しているので、まだ不安という方もそちらで練習していただける。
いまのところは、次へ進もう。参照の記述と探索経路(2) ダブルコロン演算子を用い記述・指定オブジェクトからの探索
Object
の手前までしか行われない。
例で確認してみる。module M1
module M2
::M1::M2::FOO
end
end
M1::M2
モジュールの継承チェーンは M1::M2 → Object → Kernel → BasicObject
となっていることを覚えていると思う。
しかし、 探索経路には Object
以降が含まれない。::M1::M2::FOO
の探索経路は
( M1::M2
)
となる。Object まで探索されると何が困るのか
Object
まで行かないのはどっちだっけ?」
答えはダブルコロンを使うときだが、暗記する必要はない。
事の経緯を知れば、すぐに思い出せるようになる。Object
とその先まで探索されていた。しかし、利便性に難があった。
(起票された2015年には、探索の結果トップレベルの定数を取得した場合に warning が出力されるだけであった)
そして今の実装に変更された。
終わってみれば簡単だった……か?あとがき
分かった気になって、実行してみて間違いに気付き、資料に戻って誤解に気づいて、分かった気になり、以下繰り返し。
アウトプットの盛んな社内で、ともに技術の追求を楽しんでいける仲間をお待ちしております。
キャリア採用:https://www.i3-systems.com/careers/
(Ruby Gold 対策) xxx_eval のスコープと定数定義
Module.nesting
で確認できるxxx_eval
とスコープを確認する例class C
p Module.nesting # [C]
end
C.class_eval do
p Module.nesting # []
end
C.class_eval(<<~CODE)
p Module.nesting # [C]
CODE
C.instance_eval(<<~CODE)
p Module.nesting # [#<Class:C>] instance_evalは特異クラス
CODE
class C; end
C.class_eval do
FOO = 'C' # Cの定数のつもりが、Objectの定数として定義されている
end
C.const_defined?(:FOO) # true 継承チェーンの探索のときにようやく見つかる
C.const_defined?(:FOO, false) # false 継承チェーンを辿らない設定にすると、見つからない
C.class_eval(<<~CODE)
FOO = 'C' # Cに定義されている
CODE
C.const_defined?(:FOO, false) # true Cに定義できている
(Ruby Gold 対策) 試験中のメモの取り方
CONST = 'XXX'
)を見かけたら、対応表に追加しようmodule M1
FOO = 'M1' # 追加! M1/FOO/'M1'
end
class C1
include M1
class << self
def foo
p FOO # 探索経路 (1) #<Class:C1> → C1 (2) #<Class:C1> → Object以降
end
end
end
# C1.foo # 探索! 誰も持っていない ==> NameError
class C1
FOO = 'C1' # 追加! C1/FOO/'C1'
end
C1.foo # 探索! (1)の C1 が持っていた ==> 'C1'
class << C1
FOO = '特異C1' # 追加! 特異C1/FOO/'特異C1'
end
C1.foo # 探索! (1)の #<Class:C1> が持っていた ==> '特異C1'
定数探索の練習・確認
練習1 ネストと継承の基本
module M1
class ::C1
Module.nesting # [C1, M1]
FOO # (1) C1 → M1 (2) C1 → Object以降
end
end
class C1
Module.nesting # [C1]
FOO # (1) C1 (2) C1 → Object以降
end
class C2 < C1
Module.nesting # [C2]
FOO # (1) C2 (2) C2 → C1 → Object以降
end
練習2 特異クラス
class C1
end
class << C1
def foo
Module.nesting # [#<Class:C1>, C1]
FOO # (1) #<Class:C1> (2) #<Class:C1> → Object以降
end
end
# C1.foo # (1) #<Class:C1> (2) #<Class:C1> → Object以降 ==> NameError
class C1
FOO = 'C1'
end
# C1.foo # (1) #<Class:C1> (2) #<Class:C1> → Object以降 ==> NameError
class << C1
FOO = '特異C1'
end
C1.foo # (1) #<Class:C1> (2) #<Class:C1> → Object以降 ==> '特異C1'
練習3 深いネスト
module M1
class C1
Module.nesting # [M1::C1, M1]
end
end
module M1::C1::M2
class C2
Module.nesting # [M1::C1::M2::C2, M1::C1::M2]
FOO = 'M1::C1::M2::C2'
end
end
class C2
Module.nesting # [C2]
FOO = 'C2'
end
module M1
module C1::M2
Module.nesting # [M1::C1::M2, M1]
class C2
Module.nesting # [M1::C1::M2::C2, M1::C1::M2, M1]
FOO # (1) M1::C1::M2::C2 → M1::C1::M2 → M1 (2) M1::C1::M2::C2 → Object以降 ==> 'M1::C1::M2::C2'
end
end
end
練習4 モジュールのinclude
module M1
FOO = 'M1'
class ::C1
Module.nesting # [C1, M1]
FOO # (1) C1 → M1 (2) C1 → Object以降 ==> 'M1'
end
end
class C2 < C1
Module.nesting # [C2]
# FOO # (1) C2 (2) C2 → C1 → Object以降 ==> NameError
include M1
FOO # (1) C2 (2) C2 → M1 → C1 → Object以降 ==> 'M1'
end