How Elixir Does Project and Dependency Management
I am going to come right out and say it: In less than an hour, Elixir's Mix tool won me over. Elixir seems to have done project and dependency management right.
Elixir is a Ruby-like functional language that runs in the Erlang VM. One of
Elixir's more interesting features is its built-in project and package
management tool, mix
. Over the last several weeks, I have been in many
discussions throughout the Go langauge community about what a good package
or project manager should look like. This post focuses on how another
community implemented theirs.
Elixir's heritage makes its approach to package management fascinating. Based on Erlang's VM, Elixir (like many JVM languages) has been able to leverage a mature library ecosystem. And it has tightly integrated with Erlang's hex.pm package management. But to make the language more usable, the developers have created robust tooling around project and package management that makes it so easy to work with that I can write this article as I learn the system.
The First-Class Project
Most of the languages I typically work with do not include (as part of the core distribution) project management tooling. Elixir provides a single tool, mix, that handles projects, including library dependencies. In this sense, I say that the project is a first-class citizen for the language.
Creating a new project goes like this:
$ mix create jv --module JV
The above creates a new project folder (jv
), complete with a bunch of
standard files. Since I added the --module JV
flag, it also scaffolded a
JV
module for me in the project's library.
The mix
tool includes a number of tools for working with the project,
including mix compile
, mix run
, mix clean
, and even a command for
packaging the project into an archive, mix archive.build
. There's also a
convenient tool for starting an Elixir shell inside of the project:
iex -S mix
.
One fascinating thing about Elixir is that mix
-based projects come with
three separate environments: test, dev, and prod. These largely impact the
way the project is built and executed, giving you the ability to change
configurations depending on the target for your build. (For example, the dev
environment may enable debug logging by default, while prod may disable
it.)
The Mixfile
Among various programming languages, there seem to be different approaches to whether package metadata belongs in code, or in a parseable file format.
Java, Python, PHP, and Node.js (NPM) opted for parseable formats. JSON, XML, and YAML seem to be the most popular formats.
Elixir instead declares a module, called the Mixfile
, that declares attributes
in code. It is located in a file called mix.exs
, which is an Elixir source
file.
The mix.exs
automatically generated for me by mix
looks like this:
defmodule JV.Mixfile do
use Mix.Project
def project do
[app: :jv,
version: "0.0.1",
elixir: "~> 1.0",
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
deps: deps]
end
# Configuration for the OTP application
#
# Type `mix help compile.app` for more information
def application do
[applications: [:logger]]
end
# Dependencies can be Hex packages:
#
# {:mydep, "~> 0.3.0"}
#
# Or git/path repositories:
#
# {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
#
# Type `mix help deps` for more examples and options
defp deps do
[]
end
end
Roughly, there are three sections to this file:
- Information about the project itself, and the minimum requirements to run it.
- Application configuration.
- External dependencies. (Actually, these are defined in a private function that is called when constructing the main metadata. Conventionally, they're declared as if they were separate.)
Essentially, all of the project's functional metadata (that needed to build and run) is stored in this one file.
Dependency Versioning
By default, mix
seems to support three different package sources:
- Hex, the mature "package manager for the Erlang ecosystem".
- GitHub.
- Any other git repository that is accessible by a Git URL.
Working with Hex packages is trivial. You simply supply the package name and the version(-ish) that you want to pull.
For example, I would like to install the JSON decoder called poison.
Here's what the dependencies section looks like in our mix.exs
file:
defp deps do
[{:poison, "~> 1.5"}]
end
The versioning support in Elixir is brilliantly simple to use:
- Version numbers are declared in the Mixfile (see
version
in the example above). - The versioning scheme is well-defined:
- The main version number is SemVer 2.0
- A pre-release string can be appended (
-beta.1
) - An optional build string can be appended (
+20150919
)
- Version resolution allows both exact matches and range matches
== 2.1.0
means the dependency MUST be exactly that version> 2.1.0
means the dependency must be greater than 2.1.0, and similar operators like>=
,<
, and<=
are supported- Requirments can be conjoined (
and
) or disjoined (or
) - And possibly best of all, there is a convenient "fuzzy" declaration:
~> 2.1.0
, which can be read as "anything greater than or equal to 2.1.0, as long as it's less than 2.2". Or, simpler, "Any patch release of 2.1".
So in our example above, I requested version 1.5, whatever its latest patch release is.
Dependency Tooling
From here, installing a dependency is trivially easy:
$ mix deps.get
Running dependency resolution
Dependency resolution completed successfully
poison: v1.5.0
* Getting poison (Hex package)
Checking package (https://s3.amazonaws.com/s3.hex.pm/tarballs/poison-1.5.0.tar)
Fetched package
Unpacked package tarball (/Users/mbutcher/.hex/packages/poison-1.5.0.tar)
Further management options include:
mix deps
prints a list of all dependencies for the projectmix deps.update
updates depdencies to their latest (within the version contrstraints).mix deps.compile
compiles the declared dependencies
There are also a host of hex
specific actions you can take.
While my Elixir code is clearly on the neophyte edge, dependency management seems to be a breeze.
Why Elixir/Mix Is Great
Conversations about project management or dependency management often seem to stumble into pedantic bike-shedding about the differences between project metadata, dependencies, and build tools. Elixir/Mix seems to have foregone the pedantics and just focused on the use cases.
The mix
tool is undeniably convenient. It handles all of the common cases for
managing Elixir projects. Now, I'm sure there are issues with the tool that
established Elixir developers groan about. But the tooling seems to have taken
the best parts of existing tools and approaches, and combined them into one
consistent and useable toolchain.
The result is something that feels right. And when it comes to the day-in, day-out job of building applications, the utility of the tool is what counts.
Conclusion
Elixir's Mix tool seems to genuinely make project and dependency management straightforward. In a mater of 90 minutes, I went from no knowledge of Mix to having a working and compiling project.
What is the cost of ease of use? Perhaps transparency. I am not at all sure how Mix works behind the scenes, and how hard it would be to manage dependencies without Mix. In fact, because so many of the details are handled for me, I can't say that I even know how Elixir loads packages.
But perhaps that's a small price to pay for having a system that is convenient and powerful.