エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

一緒にインストールされるパッケージによってふるまいを変える Elixir の Req パッケージの覚書

ネット上の CSV データを、Req パッケージを使ってダウンロードし、NimbleCSV でデコードしようとしていたのですが。

hex.pm hex.pm

二つのパッケージをインストールして、Req のレスポンスのボディを NimbleCSV でデコードしたら失敗し、ボディがテキストデータでないことに気がつきました。 なにやらボディの内容がリストになっています。

最初は iodata になっているのかと勘違いしたのですが、実はレスポンスのボディがすでに CSV としてデコードされ、リストのリストになっているのでした。

よくよく Req のドキュメントを調べてみると、JSON や ZIP などは自動的にデコードする仕組みになっているのですが、NimbleCSV を一緒にインストールした場合には CSV も自動的なデコードの対象になる仕組みになっていることがわかりました。

実現方法はいたって単純で、

  • Code.ensure_loaded?/1 で NimbleCSV がロードされているか調べる
  • ロードされていれば NimbleCSV を使ってボディをデコードする、ロードされていなければ何もしない

というもの。 ただし、これだけではコンパイル時に NimbleCSV が見つからないと警告が出てしまいます。 警告を抑えるために、

  • mix.exsproject/0:xref を使って対象外にする

という細工がされていました。 なお :xref の仕様を調べきれていないので具体的な機序はまだ確認できていません。

とはいえ。 実現方法がわかり実践することは可能なので、サンプルを書いてみることにしました。

最初に、二つのパッケージを利用する MyApp と、利用される Foo, Bar を作成します。

$ mix new my_app
$ mix new foo
$ mix new bar

my_app/lib/my_app.ex を編集して Foo を利用する関数を追加します。

defmodule MyApp do
  def do_something do
    Foo.do_something() |> IO.puts()
  end
end

my_app/mix.exs の依存パッケージの記述に Foo を追加します。

defmodule MyApp.MixProject do
  use Mix.Project

  # 略

  defp deps do
    [
      {:foo, path: "../foo"}
    ]
  end
end

次に foo/lib/foo.ex を編集します。

Bar がロードされているか Code.ensure_loaded?/1 で判定します。 ロードされていれば Bar の関数を実行し、そうでなければ自分で値を返します。

defmodule Foo do
  def do_something do
    if Code.ensure_loaded?(Bar) do
      Bar.do_something()
    else
      "do something by Foo"
    end
  end
end

最後に bar/lib/bar.ex を編集します。

defmodule Bar do
  def do_something do
    "do something by Bar"
  end
end

my_app ディレクトリに移動して MyApp.do_something/0 を実行します。

$ cd my_app
$ mix run -e 'MyApp.do_something()'

Foo.do_something/0 が呼び出され、do something by Foo が表示されますが、Bar が未定義であることの警告が表示されてしまいます。

==> foo
Compiling 1 file (.ex)
    warning: Bar.do_something/0 is undefined (module Bar is not available or is yet to be defined)
    │
  4 │       Bar.do_something()
    │           ~
    │
    └─ (foo 0.1.0) lib/foo.ex:4:11: Foo.do_something/0

Generated foo app
do something by Foo

そこで foo/mix.exs を編集して以下の設定を追加します。

defmodule Foo.MixProject do
  use Mix.Project

  def project do
    [
      # 略
      xref: [
        exclude: [
          Bar
        ]
      ],
      # 略
    ]
  end
  # 略
end

これで再度実行すると Foo が再コンパイルされますが、未定義の警告は表示されなくなりました。

$ mix run -e 'MyApp.do_something()'
==> foo
Compiling 1 file (.ex)
Generated foo app
do something by Foo

もう一度 my_app/mix.exs を編集して依存パッケージの記述に Bar を追加します。

defmodule MyApp.MixProject do
  use Mix.Project

  # 略

  defp deps do
    [
      {:foo, path: "../foo"},
      {:bar, path: "../bar"}
    ]
  end
end

もう一度再実行します。

MyApp や Foo はコンパイルされず、Bar だけがコンパイルされるのがわかります。 そして MyApp.do_something/0 から Foo.do_something/0 が、Foo.do_something/0 から Bar.do_something/0 が呼び出され、最終的に do something by Bar が表示されました。

$ mix run -e 'MyApp.do_something()'
==> bar
Compiling 1 file (.ex)
Generated bar app
do something by Bar