🍣

Elixir 1.12で追加されたMix.install/2の使い道

2021/08/28に公開

Elixir 1.12で追加されたMix.install/2という関数が、どういう時に便利なのかについて、確認してみたいと思います。

Mix.install/2とは

Elixir 1.12からMix.install/2という関数が追加されました(ドキュメント: Mix — Mix v1.12.2)。

それまでは、外部のモジュールを使いたい時はmix.exsに追加した上で、mixプロジェクトとして起動する必要がありました。

Mix.install/2を用いると、mix.exsに依存を追加せずとも、Elixirスクリプト内で以下のように書いておくと、実行時にモジュールのインストールが行われ、スクリプト内で利用できるようになりました。

Mix.install([
  :decimal,
  {:jason, "~> 1.0"}
])

初回実行時には依存モジュールのダウンロードが実行されるため、時間がかかります。次回以降はキャッシュから読み込むので、速いです。

Elixirスクリプトで依存モジュールを読み込む

スクリプト内でのMix.install/2のユースケースと実行例については、以下の記事をご参照ください。

Erlangノードに依存モジュールを注入する

Erlangノードとは、他のErlangノードと通信可能なErlangのランタイムのことをいいます(Elixirでいうと、iex --sname fooとかして起動すると立ち上がるものです)。

分散されたErlangシステムは互いに通信するいくつものErlangランタイムシステムを含んでいます。 これらのそれぞれのランタイムはノードと呼ばれます。

OTPディストリビューション · Elixir School

このノードに対して、依存モジュールを注入してやるということをしてみましょう(「注入」といっても、対象となるノード上でMix.install/2を実行するということなので、コード自体を「注ぎ入れる」というのとはイメージが違いますが)。

実際にやってみる

Erlangノードをふたつ立ち上げて、実験してみましょう。

一方のターミナルで、以下のようにしてノードを起動します。

$ iex --sname node1
iex(node1@keyaki)1>

もう一方でも、以下のようにしてノードを起動します。

$ iex --sname node2
iex(node2@keyaki)1>

node1からnode2に接続します。ちゃんと繋がっているのも確認しましょう。

iex(node1@keyaki)1> Node.connect(:node2@keyaki)
true
iex(node1@keyaki)2> Node.list
[:node2@keyaki]

ここで、node2でJSONを扱うライブラリであるjasonを用いて、JSONデータをデコードしてみることを試みます。

iex(node2@keyaki)1> Jason.decode("{\"foo\": \"bar\"}")
** (UndefinedFunctionError) function Jason.decode/1 is undefined (module Jason is not available)
    Jason.decode("{\"foo\": \"bar\"}")

このように、エラーになります。jasonは、標準では読み込まれない依存モジュールなので、当然です。

ここで、node1からnode2に対して、この依存モジュールを注入してみましょう。

iex(node1@keyaki)3> Node.spawn(:node2@keyaki, fn -> Mix.install([:jason]) end)
#PID<11553.121.0>
Resolving Hex dependencies...
Dependency resolution completed:
New:
  jason 1.2.2
* Getting jason (Hex package)
==> jason
Compiling 8 files (.ex)
Generated jason app

何やら依存モジュールがHexリポジトリからダウンロードされてインストールされたようです。node1でそれが行われているように見えますが、実際にはnode2で実行された内容がnode1側に印字されているだけです。

node2でもう一度JSONをデコードしてみましょう。

iex(node2@keyaki)1> Jason.decode("{\"foo\": \"bar\"}")
{:ok, %{"foo" => "bar"}}

今度はエラーなくJSONをデコードできました。

Mix.install/2の制約

Mix.install/2のドキュメントには、以下の通りの記載があります。

This function can only be called outside of a Mix project and only with the same dependencies in the given VM.

Mix — Mix v1.12.2

つまり、mixプロジェクトとして起動されたノードで実行することはできません。すでに読み込まれた依存関係にあとから追加してしまうと、整合性が取れなくなってしまい得るからなのでしょう。

同様に、一度Mix.install/2を実行した後に、別の依存モジュール(や、同じものでもバージョン違い)をインストールすることもできません。

iex(node2@keyaki)2> Mix.install([:decimal])
** (Mix.Error) Mix.install/2 can only be called with the same dependencies in the given VM
    (mix 1.12.2) lib/mix.ex:454: Mix.raise/2
    (mix 1.12.2) lib/mix.ex:544: Mix.install/2

このように、エラーになります。

これは何のためにあるのか

端的には、まずはLivebookのユースケースを満たすために開発されたものなのだろうと思います。Livebookは、Elixirで実装された、JupyterLabのようにドキュメントツールと統合されたElixirのコード実行環境です。

Livebookでは、デフォルトのElixir Standaloneモードでは、ノートブックごとに異なるノードを起動して、そのノード上でコードを実行します。なぜそんなことをしているかというと、Livebookが動いているVM上でコードを実行すると、異なるノートブック間で書いたコードが影響しあってしまい、不都合だからです(Nervesのような環境で動かすために、あえてそれを可能にするEmbeddedモードというのもあります)。

そのように、コードの実行を別途起動したノード上で行うことで、ノートブックのコードの実行環境がそれぞれ分離された状態を担保できるわけです。また、そのことにより、Elixir StandaloneモードではMix.install/2によってノートブック内で動的に依存モジュールを読み込むことができます。まっさらなノードを起動した上で、ノートブックのコードに必要な環境をいちから作れるようにしているわけですね。

このように、コードをメインのノードとは分離された環境で実行しつつ、実行する側のノードにおいても必要なモジュールを簡単に追加できるようにするのに、Mix.install/2は便利です。Livebookを用いない場合でも、上記の「Erlangノードに依存モジュールを注入する」で示したように、コードをノード間で分散実行したい場合に、コードの実行に必要なモジュールをセットアップするのに使えます。Elixirによる分処理散基盤を、より便利に使うのに役立つ機能であると思います。

おわりに

本記事では、Mix.install/2とは何かについて説明しつつ、Elixirの分散処理基盤における使い道について検討しました。Elixirって、とっても便利ですね。

Discussion