iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🍣

Use Cases for Mix.install/2 Introduced in Elixir 1.12

に公開

I would like to explore when the Mix.install/2 function, added in Elixir 1.12, proves to be useful.

What is Mix.install/2?

Mix.install/2 was added in Elixir 1.12 (Documentation: Mix — Mix v1.12.2).

Until then, if you wanted to use external modules, you needed to add them to mix.exs and then start the application as a mix project.

By using Mix.install/2, you can write the following in an Elixir script without adding dependencies to mix.exs. This allows modules to be installed at runtime and used within the script.

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

The initial execution takes time because the dependencies must be downloaded. Subsequent runs are fast because they are loaded from the cache.

Loading Dependencies in Elixir Scripts

For use cases and execution examples of Mix.install/2 within scripts, please refer to the following articles (in Japanese):

Injecting Dependency Modules into Erlang Nodes

An Erlang node refers to an Erlang runtime that can communicate with other Erlang nodes (in Elixir terms, this is what starts when you run something like iex --sname foo).

A distributed Erlang system consists of a number of Erlang runtime systems communicating with each other. Each of these runtime systems is called a node.

OTP Distribution · Elixir School

Let's try "injecting" dependency modules into such a node (although "injecting" here means executing Mix.install/2 on the target node, so the mental image might be slightly different from physically "pouring" code into it).

Giving it a try

Let's experiment by starting two Erlang nodes.

Start a node in one terminal as follows:

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

Start another node in a second terminal:

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

Connect to node2 from node1. Let's also verify that they are properly connected.

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

Now, let's try to decode some JSON data on node2 using jason, a library for handling 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\"}")

As expected, this results in an error. This is because jason is a dependency module that is not loaded by default.

Now, let's inject this dependency module into node2 from node1.

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

It seems the dependency module was downloaded from the Hex repository and installed. Although it looks like this is happening on node1, the output from node2 is actually just being printed on the node1 side.

Let's try decoding the JSON on node2 again.

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

This time, the JSON was decoded without any errors.

Constraints of Mix.install/2

The Mix.install/2 documentation states the following:

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

In other words, it cannot be executed on a node started as a Mix project. This is likely because adding dependencies after they have already been loaded could lead to consistency issues.

Similarly, once Mix.install/2 has been executed, you cannot install other dependency modules (or different versions of the same one).

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

As shown, this results in an error.

What is this for?

Simply put, I believe it was primarily developed to meet the use cases of Livebook. Livebook is an Elixir code execution environment integrated with documentation tools, much like JupyterLab, and is itself implemented in Elixir.

In Livebook's default "Elixir Standalone" mode, it starts a separate node for each notebook and executes code on that node. This is done because executing code on the same VM where Livebook is running would cause code from different notebooks to interfere with each other, which is undesirable (though an "Embedded mode" exists specifically to allow this for environments like Nerves).

By executing code on a separately started node, the isolation of each notebook's execution environment is guaranteed. This allows Mix.install/2 in "Elixir Standalone" mode to dynamically load dependency modules within a notebook. It enables setting up the necessary environment for a notebook's code from scratch on a fresh node.

As such, Mix.install/2 is useful for easily adding required modules while executing code in an environment isolated from the main node. Even without using Livebook, as shown in the "Injecting Dependency Modules into Erlang Nodes" section, it can be used to set up the necessary modules when you want to distribute code execution between nodes. I believe this is a feature that helps make Elixir's distributed processing infrastructure even more convenient to use.

Conclusion

In this article, I explained what Mix.install/2 is and explored its uses within Elixir's distributed processing infrastructure. Elixir is truly convenient, isn't it?

Discussion