DEV Community

loading...
gumi TECH Blog

Elixir: function_exported?/3 する前には Code.ensure_loaded/1 を呼び出す

gumitech profile image gumi TECH ・1 min read

本稿は「function_exported?/3 する前には Code.ensure_loaded/1 を呼び出そう」をもとに加筆・補正し、文章を整えました。

function_exported?/3でビヘイビアの関数の実装を確かめる

ビヘイビアが実装されたモジュールから、それらの関数を呼び出すときは、予めfunction_exported?/3によりその関数が備わっているかどうか確かめます。たとえば、つぎのようなFooビヘイビアを実装したFooImplモジュールがあるとします。

defmodule Foo do
  @callback foo() :: :ok
end

defmodule FooImpl do
  @behaviour Foo

  @impl Foo
  def foo() do
    :ok
  end
end

呼び出す関数からは、つぎのようにfunction_exported?/3で引数のモジュールの関数がエクスポートされているか、つまりFooビヘイビアが実装されているかを確認してから呼び出します。これでまったく問題のないコードにみえるかもしれません。

defmodule Bar do
  def call_foo(mod) do
    if not function_exported?(mod, :foo, 0) do
      raise "Foo behaviour is not implemented"
    end
    mod.foo()
  end
end

call_foo(FooImpl)

しかし、このコードは、mixから起動したときに動作しない場合があります。

なぜ動作しないのか

ドキュメントのfunction_exported?/3の項には、つぎのような注意書きがあります。引数のモジュールが予めロードされていない場合、function_exported?/3はモジュールの読み込みはしません。つまり、関数が実装されていない場合だけではなく、モジュールがまだロードされていないときもfalseが返されるのです。

Note that this function does not load the module in case it is not loaded.

モジュールを用いるには、そのモジュールが事前にErlang VMにロードされていなければなりません。それを理解するには、Erlangがモジュールをロードする戦略を知る必要があります。戦略はつぎのふたつです(Erlang VMのデフォルトは後者)。

  • 組み込みモード アプリケーションを起動したときに、すべてのモジュールがロードされます。
  • 対話モード 最初にそのモジュールの関数を呼び出したときに、そのモジュールがロードされます。

mixからアプリケーションを動かした場合には 対話モードになります。そのため、前掲のコードを動かしたとき、FooImplモジュールの関数をまだどれも呼んでいなければ、function_exported?/3falseを返してエラーになるのです。他方で、先に実行されたコードがFooImplモジュールの関数を呼び出していると、function_exported?/3の戻り値はtrueとなり、正しくmod.foo()が呼ばれます。

つまり、mixから起動したときは、どのコードをとおったかによって動作は変わってしまいます。これが「動作しない場合」があると述べた理由です。なお、Distilleryで生成すると、デフォルトで組み込みモードになります。

予めモジュールのロードを確かめる

ドキュメントのfunction_exported?/3の項は、Code.ensure_loaded/1の参照を促します。引数のモジュールがロードされているかどうか確かめて、結果がまだであればロードしてくれる関数です。つぎのようにCode.ensure_loaded/1の呼び出しを書き加えると、ビヘイビアのモジュールの読み込みが実行されます。

defmodule Bar do
  def call_foo(mod) do
    Code.ensure_loaded(mod)
    if not function_exported?(mod, :foo, 0) do
      raise "Foo behaviour is not implemented"
    end
    mod.foo()
  end
end

call_foo(FooImpl)

もっとも、モジュールの読み込みは、何らかの理由で失敗するかもしれません。そのときの戻り値は{:error, reason}になりますので、エラーの分岐を書いた方がより厳密です(成功であれば{:module, module})。もっとも、その場合にはつぎのfunction_exported?/3falseを返して、結局ビヘイビアを実装していないというエラーになります。この例ような簡単なコードでしたら、これで済むこともあるでしょう。

Discussion (0)

pic
Editor guide