These are some things I commonly use in my elixir development workflow that might be interesting for someone.

Managing multiple versions of elixir and erlang

When you have to work in more than one project at time that could probably means you have to handle different elixir and erlang versions so installing the default version that your OS provides won't be helpful. Here is where asdf shines to solves this problem, asdf allow us to have different versions of elixir, erlang and other languages in the same machine so we can easily switch between them.

In macOS you can install it with brew install asdf and then follow the instructions that the installer prints out to set up the PATH, more info in its Github page https://github.com/asdf-vm/asdf

Now we have asdf installed we need to install the plugins to handle erlang and elixir, we can install them with:

asdf plugin add erlang
asdf plugin add elixir

Once we have those installed we need to define which versions we're going to use in our project, there is more than one way to do that:

Using environment variables

We can set up the required versions by defining environment variables with the ASDF_ prefix so if we need version elixir 1.10 we need to define the variable ASDF_ELIXIR_VERSION with the value 1.10 the same applies for erlang or other programming languages as well.

For example we can define variables for elixir and erlang as the example below:

export ASDF_ELIXIR_VERSION=1.10.3-otp-22
export ASDF_ERLANG_VERSION=22.3

To handle environment variables in a easy way we can use direnv, it allows to define environment variables in a file .envrc and it will loaded automatically as soon as we enter to out project folder.

File based config file

asdf allow us to define a .tool-versions file where we can put all the versions needed for our project, we can define one as the example below:

erlang 23.0.2
elixir 1.10.4-otp-23

Creating a new project

Because we have first to define the versions used in a project we can't just run mix new my_app because asdf doesn't know yet which versions we want. To do this we have 2 options:

  • Define global versions of elixir and erlang using for example asdf global elixir 1.9.0 and the same for erlang asdf global erlang 22.3 and then we can execute mix new my_app normally
  • Define the versions just for the mix new command execution, for example ASDF_ELIXIR_VERSION=1.9.0 ASDF_ERLANG_VERSION=22.3 mix new my_app, this way we don't affect the global scope and then we can define these same versions inside the created project.

I like the second one because I don't have to change the global version each time I want to create a new project and I can easily access to that command from bash history.

Notes about erlang compilation

asdf uses kerl under the hood to handle erlang compilation and when we are installing a new version it will ask for a java installation 😕, to avoid this behaviour we can define the following environment variable:

export KERL_CONFIGURE_OPTIONS="--disable-debug --without-javac"

Ecto database url

If we are dealing with databases in our project we will probably be using Ecto. And Ecto allow us to define database credentials in two ways, the first one is define them separately as the example below:

config :my_app, Repo,
  database: "ecto_simple",
  username: "postgres",
  password: "postgres",
  hostname: "localhost"

And the second one is using a unique parameter:

config :my_app, Repo,
  url: "postgres://postgres:postgres@localhost/ecto_simple"

This is my favorite option for these reasons:

  • Just one value to maintain
  • This format is also accepted in psql, for example we can execute psql postgres://postgres:postgres@localhost/ecto_simple and we're connected to the database. I just discovered this a few weeks ago 😅
  • We can change credentials for example when we're running a mix command just prepending the value DATABASE_URL=postgres://postgres:postgres@localhost/test_db mix something in the case we're loading it from an environment variable

Then if you have the connection url in a variable called DATABASE_URL, using direnv of course 😉, you can just execute psql $DATABASE_URL to database session.

Using iex

Enable shell history

A cool feature of elixir is iex, you can load modules, recompile them and so on, but when sometimes we execute "large" pieces of code or some cases that we're trying out to understand the code or something else and when we have to restart the session we lost all the history 😢, we can avoid this by adding a flag -kernel shell_history enabled in the environment variable ERL_AFLAGS before we start our iex session. I just put following code in my .zshrc to have it enabled for all my projects:

export ERL_AFLAGS="-kernel shell_history enabled"

Preload aliases

Another thing than could be annoying to deal with is aliasing a large module name, for example if we have MyApp.Contexts.Authentication.User and we are using this module pretty often it could be easier to have it already loaded when we start a iex session, we can make this by defining a .iex.exs file in the project root with the desired aliases, for example:

alias MyApp.Contexts.Authentication.User

And now when we start a new iex session we will have that module aliased from the beginning so we can use User.whatever without a problem.

Keep in mind that even if we can make an alias(a module name is just an atom) when we starting a session using just iex we cannot access to its functions. We need to start our iex session using iex -S mix

Recompiling modules

Within a iex sessions we can recompile a module just writing r module_name and if the want to recompile the whole project we can execute recompile, this is useful when we are making some changes in the code and we need to test it right away with all the values that we already had defined. It's also called "REPL based development" and it's most used with lisp based programming languages but having iex in elixir we can use those nice features as well.

Mix tasks

These are tasks that mix can run, duhh.. But we can create them and use them in our projects. For example maybe we are debugging some code and we don't want to execute a long process(business process) instead of that we can just extract some function calls and execute them from a mix task using existing data. We can create a mix task with the following code:

defmodule Mix.Tasks.Foo do
  @moduledoc false
  use Mix.Task

  def run(_args) do
    Application.ensure_all_started(:my_app)
    IO.puts("runnning...")
  end
end

We have to name this file foo.ex and place it inside lib folder and now we can run mix foo and we'll get a running... message.

I use this many times, actually I have some defined tasks in many projects than I reuse to debug some workflows.

I know that we "should" be defining the cases that we are debugging in a test, run it and then try to fix the code and then run the tests again but this way works for me so I'm OK with that 🙃

Working with local third party libraries

In some cases we could found some weird behaviour, a bug of just want to know a little more deep about how a third party library works. In that case it could be difficult to setup a local version of a library that we use in our project.

I remember using just pip install -e path_to_library in python and just starting to changing the library code.

In elixir when we want to install a local version of a library we can specify the path of it in the mix.exs file, for example:

defmodule MyApp.MixProject do
  use Mix.Project

  def project() do
    [
      app: :my_app,
      version: "0.0.1",
      elixir: "~> 1.0",
      deps: deps(),
    ]
  end

  def application() do
    []
  end

  defp deps() do
    [
      {:ecto, "~> 2.0"},
      {:postgrex, "~> 0.8.1"},
      {:ecto_sql, path: "ecto_sql_local_path"}
    ]
  end
end

In this case we're telling our project to install ecto_sql from the given path, this will work but just the first time because it will load and compile ecto_sql at the beginning and then when we're making some changes in the code placed in ecto_sql_local_path these changes won't be recompiled automatically because mix is only watching for changes inside our project. In this case we can force to recompile some modules by using for example r Ecto.Migrator from within an iex session but if we are modifying more modules it would be tedious to recompile manually every one of them, for this case we can define a Recompiler module that make this work for us, name it as you want, this will contains:

defmodule Recompiler do
  def run do
    modules_to_recompile = [
      Ecto.Migrator,
      Ecto.SomeOtherModule
    ]

    for module <- modules_to_recompile do
      IEx.Helpers.r(module)
    end
  end
end

We can place this module somewhere inside our lib folder and when we call Recompiler.run from within a iex session it will recompile all the defined modules.