1
0

未定義の定数参照が発生して困った話

Last updated at Posted at 2023-11-14

はじめに

class, moduleの参照で躓き、普段あまり定数がどう使用されどう管理されているかを調査することになったので、備忘録もかねて結果を残します。

※ 2023/11/16 2:00 記事内容修正
誤った内容を記事にしてしまっていたため、内容の修正、追記を行いました。

環境

ruby 2.6.6
rails 6.1.7.2

ファイル構成

models/AAA.rb
class AAA
  include BBB::CCC
end
lib/BBB/CCC.rb
module BBB
  module CCC
  end
end
SDK/BBB.rb
module SDK
  module BBB
  end
end

起きたこと

railsプロジェクト内でSDKを読み込んで開発を進める中、AAAクラスを使用する以下のような処理で名前参照エラーが発生。
SDK::BBB::CCCは定義しておらず、BBB::CCCモジュールの参照がおかしなところから走っているように見える。
SDK内のコードを確認するとBBBクラスが存在していたが、どうしてSDK内のBBBからの参照となったのか定数参照の挙動を調査する。

require 'SDK' #sdkの読み込み
AAA #AAAモデルの参照
uninitialized constant SDK::BBB::CCC (NameError)

定数定義について

公式ドキュメントに下記の記述がある。
またクラス定義式はクラスオブジェクトの生成を行うと同時に、名前がクラス名である定数にクラスオブジェクトを代入する動作をします。
https://docs.ruby-lang.org/ja/latest/doc/spec=2fvariables.html#:~:text=%E3%81%AB%E3%81%AA%E3%82%8A%E3%81%BE%E3%81%99%E3%80%82-,%E5%AE%9A%E6%95%B0,-%E4%BE%8B

試しにBBB::CCCクラスを以下のようにrailsコンソールで呼び出してみる。

irb(main):020:0> BBB::CCC
=> BBB::CCC
irb(main):021:0> CCC = 123
=> 123
irb(main):022:0> BBB.class
=> Module
irb(main):023:0> BBB::CCC.class
=> Module
irb(main):024:0> CCC.class
=> Integer

確かにモジュール名に対応する定数にオブジェクトが代入されており、定数CCCは定数BBB::CCCとは別物である(当然)

定数は宣言したネームスペース内で管理されているため、BBBモジュールの中でCCCという定数が衝突しない限りは問題は発生しない。試しにBBBモジュール内に定数CCCを追加してみるとエラーになった。

irb(main):001:1* module BBB
irb(main):002:1*   CCC = 123
irb(main):003:2*   module CCC
irb(main):004:1*   end
irb(main):005:0> end
Traceback (most recent call last):
(irb):3:in `<module:BBB>': CCC is not a module (TypeError)

定数参照について

moduleやclassを指すオブジェクトはすべて定数によって管理されている。ということは、railsのmodelを使用した開発もすべて定数を参照してセットされているオブジェクトを触っていることになる。
以下のようなコードがあったとき定数AAAは未定義に見えるが、実際はモデルのインスタンスが作成できる。定数AAAにいつオブジェクトが代入され、どこで管理されているか確認する。

hoge_controller
class HogeController < ApplicationController
  def show
    model = AAA.new
  end
end

railsにはconfig.autoload_pathsという機能があり、app/models以下などが自動で読み込む対象に指定されている。

コード上で未定義の定数宣言があった場合、config.autoload_pathsで指定されているディレクトリから宣言された定数のネームスペースと一致するディレクトリ構造のファイルを見つけ出し、Objectクラス内に定義を行う。
自身のオブジェクト内に定義がなければ上位のモジュール内を探索を繰り返していき、見つからなければ自動読み込みに合致するファイルがあるか探索して定数宣言を行なっている。

定数参照の順序

①BBB::CCCモジュール内の定数探索
②BBBモジュール内の定数探索
③Objectクラス内の定数探索
④config.autoload_pathsから対象となるmodule, classの探索

何が原因でNameErrorが発生していたか

これまでの調査で定数はネームスペースごとに管理されており、未定義の定数の場合は上位のモジュールを探索しにいくことがわかった。
そのため、当初の問題のNameErrorは通常起きえないように思える。
requireされて読み込まれるSDKの中身を確認してみる以下のファイルがあった。。。

sdk/include.rb
include SDK

これはObjectクラスにSDKモジュールをMix-inするという記述になっている。
SDKモジュールにはSDK::BBBモジュールのオブジェクトがセットされている定数BBBが定義されており、それがObjectクラスに定義されることになるため、定数BBBの呼び出しでSDK::BBBモジュールのオブジェクトを参照できるようになってしまう。

irb(main):007:0> include SDK
=> Object
irb(main):007:0> Object::BBB
=> SDK::BBB
irb(main):008:0> BBB
=> SDK::BBB

そのため、AAAクラスでのincludeで起きていたNameErrorはObjectクラスに定義済みの定数があったため、そちらを参照したことで意図しない挙動をしているように見えていた。

models/AAA.rb
class AAA
  include BBB::CCC
  # ① BBBの探索
  # ② Objectクラス内にBBBが定義済みなので探索終了
  # ③ BBBにはSDK::BBBのオブジェクトがセットされているのでSDK::BBB::CCCを参照する
  # ④ SDK::BBB::CCCは未定義なのでincludeできずエラー
end

まとめ

SDK読み込み時の挙動を追えておらず、上位のクラスに既に定数定義がされていただけだった。
意図しない参照が走っている場合は問題のモジュールがいつどこに宣言されているのかを確認するようにしたい。

おわりに

普段あまり意識せずに定数を使っていたことに気づけ、基礎的な定数管理について学べた良い機会になりました。
他にも意識せずに使っている機能があると思うので、少しずつruby, railsを理解していこうと思います。

@scivola さん
ご指摘ありがとうございました。
おかげさまで間違った内容の記事のままにならずに済み、私個人としてもしっかりと勉強ができる機会になりました。
重ねてお礼申し上げます。

1
0
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0