Photo by LĂZĂRESCU ALEXANDRA on Unsplash
Last year, in October 2020, Elixir 1.11 was released 🎉 I looked at one of the compiler improvements, Exports dependencies, including its behavior.
How the "Exports dependencies" improves compilation?
Prior to Elixir 1.11, if module A
depends on module B
by import
or require
, module B
was considered as Compile time dependencies of module A
.
Since Elixir 1.11, if module A
uses only the public interface of module B
(struct or public functions) is now Exports dependencies, and there is no need to recompile module A
unless the public interface of module B
changes. 1
This will reduce the number of files that need to be recompiled when the source code is changed, shortening the "fix - build - run" cycle. This is great because the compilation time required for a build is an important indicator that directly affects development productivity.
Try Exports dependencies in example
You can get an idea about Exports dependencies from coding an example. Consider the two modules below. 2
moduleA.ex
defmodule ElixirV11.ModuleA do
alias ElixirV11.ModuleB
import ModuleB, only: [fetch_name: 1]
def hello(%ModuleB{} = m) do
case fetch_name(m) do
{:ok, name} ->
IO.puts("Hello, #{name}!")
:error ->
IO.puts("Who?")
end
end
end
moduleB.ex
defmodule ElixirV11.ModuleB do
defstruct name: nil
def new(name), do: %__MODULE__{name: name}
def fetch_name(%__MODULE__{name: nil}), do: :error
def fetch_name(%__MODULE__{name: name}), do: {:ok, name}
end
ModuleA depends on the struct and fetch_name/1
functions of ModuleB. This is why import ModuleB, only: [fetch_name: 1]
in moduleA.ex
was Compile time dependencies before Elixir 1.10. However, since these two are public interfaces, starting with Elixir 1.11, they will be Exports dependencies.
So, when you change moduleB.ex
, you need to recompile moduleA.ex
in Elixir 1.10, but not in Elixir 1.11.
# Elixir 1.10
$ touch lib/moduleB.ex
$ mix compile --verbose
Compiling 1 file (.ex)
Compiled lib/moduleB.ex
Compiled lib/moduleA.ex
# Elixir 1.11
$ touch lib/moduleB.ex
$ mix compile --verbose
Compiling 1 file (.ex)
Compiled lib/moduleB.ex
Let's change the implementation of ModuleB
without changing the public interface.
moduleB.ex
defmodule ElixirV11.ModuleB do
defstruct name: 1
def new(name), do: %__MODULE__{name: check_name!(name)}
def fetch_name(%__MODULE__{name: nil}), do: :error
def fetch_name(%__MODULE__{name: name}), do: {:ok, name}
defp check_name!(%__MODULE__{name: name}) when is_binary(name), do: name
end
Added a private function called check_name!/1
and changed it to be used in new/1
. Since this does not change the public interface, it should not need to be recompiled.
# Elixir 1.10
$ mix compile --verbose
Compiling 1 file (.ex)
Compiled lib/moduleB.ex
Compiled lib/moduleA.ex
# Elixir 1.11
$ mix compile --verbose
Compiling 1 file (.ex)
Compiled lib/moduleB.ex
As expected, ModuleA
do not need to be compiled in Elixir 1.11.
Exports dependencies in a real project
The commit says as follows:
making imports more feasible for large projects.
As this commit says, it will be easy to use import/2
in large projects.
In fact, in the project I'm working on now, I changed the modules that manipulate Repo, which is imported at various points, to find out the number of files that are recompiled.
- Elixir 1.10 - 48
- Elixir 1.11 - 14
The number of recompiled files had decreased dramatically 🎉
-
However, if you are using macros, it will be Compile time dependencies. This is explained in detail in Dependencies types on the mix xref page. ↩
-
The change to the compiler is probably this. Do not make requires/imports compile-time dependencies · elixir-lang/elixir@9a6db66 ↩
Top comments (0)