Ruby gems内でファイルをロードする際に気を付けること

おしごとでちょっぴりハマったので記す。

相対パスをそのまま書くとコケる

例えばこんな感じのGemを実装したとする。

reqiure 'yaml'
 
module Hoge
  class Fuga
    SampleData = YAML.load_file('./sample_data.yml')

    def self.fuga
      ...
    end
  end
end

sample_dataYAMLファイルに定義しておいて、Gem内で利用することを考える。 これはRuby Gemsの開発中は特に何の問題も発生しない。

しかし、いざこのGemを公開し、bunlderでインストールして別のRubyプログラムで利用すると困ったことになる。

require 'hoge'
 
class Foo
  def bar
    Hoge::Fuga.fuga
    ...
  end
end
$ bundle exec ruby sample_code.rb
#=> Errno::ENOENT (No such file or directory

これはRubyが、プロセスが実行された場所、この場合ではsample_code.rbが存在するディレクトリからの相対パスでファイルを探しに行こうとしに行ってしまうために起こる。 そのため、何らかの方法でGemファイルのソースがある場所からの相対パスでファイルを指定してあげる必要がある。

ここで役立つのが、__dir__メソッド。見た目がなんか特殊な変数っぽく見えるけど、実はこれKernel#__dir__というメソッドとして実装されている。

現在のソースファイル(FILE)のあるディレクトリ名を正規化された絶対パ スで返します。シンボリックリンクも解決されます。また、FILEnil の場合は nil を返します。

上記の通りこの__dir__を呼び出すとソースファイルのあるディレクトリ名を正規化された絶対パスで返してくれる。 これをさらにFile#joinと組み合わせることで、Gemのディレクトリ内に存在するファイルを読み込むことができるようになる。

YAML.load_file(File.join(__dir__, 'sample_data.yml'))

ちなみに、Pythonでは__file____dir__は特殊メソッドとして実装されている。

docs.python.org

また、PHPではマジカル定数という名前で利用されている。

https://www.php.net/manual/ja/language.constants.predefined.php

余談: __dir__ってなんでどこでも使えるの?

__dir__Kernelのメソッドで、Objectが継承しているから

KernelObjectが継承しており、Rubyのほとんどのオブジェクトは継承チェーンをたどってKernel#__dir__を利用できる状態になっている。 さらに、__dir__のレシーバを省略すると、暗黙的にselfをレシーバとするのだが、トップレベルの場合はmainという名前のObjectオブジェクトがレシーバとなるため、トップレベルでも、そうでなくても、__dir__をレシーバなしで変数のように呼び出すことができる。

puts "self:#{self}"
puts "self.class:#{self}"
puts "self.class.ancestors:#{self.class.ancestors}"
puts self.private_methods.grep(/__dir__/)

#=>
self:main
self.class:Object
self.class.ancestors:[Object, Kernel, BasicObject]
__dir__

Kernelモジュールには他にもいくつかメソッドが定義されていて、__dir__の他にも__method____callee__などが使える。

docs.ruby-lang.org

上記のページを読むと分かるが、__FILE__はKernelモジュールのメソッドではないらしい。

irb(main):001:0> Kernel.methods.grep(/__FILE__/)
=> []
irb(main):002:0> self.private_methods.grep(/__FILE__/)
=> []

mainのメソッドとしても発見できないため、特別な定数として実装されてるかもしれない。*1

おわりに

メタプログラミングRubyを最近ちょくちょく読んでるおかげで「なんでRubyではこう書かないといけないんだっけ?」が少しずつ分かる機会が増えてきたように感じる。近いうちに読書メモも残しておきたい。

*1:mainに対してprivate_methodsを呼んでいるのは、モジュール関数がprivateメソッドとして利用可能になるため