Elixir Module Attributes - Alchemy 101: Part 1

2016-11-08 by Thomas Hutchinson

This is Part 1 in our Alchemy 101 series. Catch up on Part 2: From Elixir Mix Configuration to Release Configuration and Part 3: Fault Tolerance Doesn’t Come Out Of The Box.

Elixir is a joy to work with, an easy installation that comes packaged with a build tool, mix. In minutes you will be creating your own applications and performing releases. Over time you will question how and why things work and behave the way they do. You may even wonder if there are better ways to do things. This is where Alchemy 101 comes in. Each edition of Alchemy 101 will address common questions that are being asked throughout the community.

Today we will be looking at when module attributes are evaluated and what this means. Take a look at the Set up section and then proceed to Module Attributes.

Set up

You can follow along with the examples. You will require elixir and to perform the following steps. First create a new mix project.

mix new my_app --module Twitter cd my_app

Next add distillery (for creating releases) to mix.exs as a dependency.

defp deps do [{:distillery, "~> 0.10.1"}] end

Then download distillery and create the release configuration.

mix deps.get mix release.init

The rest of the blog assumes that you are in the my_app directory.

Module Attributes

Module attributes can be used to annotate your module (e.g. with documentation) and add constants. Module attributes are evaluated at compile time, not runtime. Not knowing this can lead to confusion. All occurrences of the module attribute are replaced with whatever it evaluates to at compile time. I will demonstrate this below using a pattern that I’ve seen a few times.

Open lib/twitter_client.ex and add the following to it.

defmodule Twitter do require Logger @twitter_client Application.get_env(:my_app, :twitter_client) def log_twitter_client do Logger.info("Using #{@twitter_client}") end end

There will be 2 possible values for the application’s environment configuration: twitter_client, TwitterClient and MockTwitterClient. A small side note, an application’s environment configuration shouldn’t be used for global variables. The first value is for production and the second is for test and dev. The value used will be determined by the mix config file imported by config.exs. Go ahead and add the following files.

# config/config.exs use Mix.Config import_config "#{Mix.env}.exs" # config/prod.exs use Mix.Config config :my_app, twitter_client: TwitterClient # config/dev.exs use Mix.Config config :my_app, twitter_client: MockTwitterClient

So when MIX_ENV (environmental variable) is dev then config.exs will import dev.exs, when prod it will import prod.exs.

Go ahead and create the release and start it in the console.

mix release rel/my_app/bin/my_app console

Now check to see if the configuration has been applied.

iex(my_app@127.0.0.1)1> Twitter.log_twitter_client() 09:32:16.220 [info] Using Elixir.MockTwitterClient

What? Why is it using the MockTwitterClient? That’s strange. Lets check sys.config (generated from the release).

cat rel/my_app/releases/0.1.0/sys.config [{sasl,[{errlog_type,error}]}, {my_app,[{twitter_client,'Elixir.MockTwitterClient'}]}].

So the value of twitter_client is MockTwitterClient. This is because we ran ‘mix release’ without specifying prod as MIX_ENV (defaults to dev). No big deal, lets just change sys.config to the following, restart the application and check again.

[{sasl,[{errlog_type,error}]}, {my_app,[{twitter_client,'Elixir.TwitterClient'}]}]. rel/my_app/bin/my_app console iex(my_app@127.0.0.1)1> Twitter.log_twitter_client() 09:32:16.220 [info] Using Elixir.MockTwitterClient

The mock is still showing, but why? As mentioned before module attributes are evaluated at compile time, not runtime. All occurrences of @twitter_client at compile time were replaced with MockTwitterClient, it just so happens that this value came from a configuration file thus giving us the illusion that it can be changed.

To have TwitterClient we must create another release and set MIX_ENV to prod.

MIX_ENV=prod mix release

For good measure place this into a script/Makefile/something that prevents this from happening again. An alternative is to use a function instead giving you the luxury of changing the value in sys.config post release.

def twitter_client do Application.get_env(:my_app, :twitter_client) end

The takeaway from this is: when using module attributes, always be aware of what they evaluate to, making a wrong assumption can result in having to recompile your project.

Stay tuned for the next edition we will be looking at what really happens to your mix configuration when you create a release.

This is Part 1 in our Alchemy 101 series. Catch up on Part 2: From Elixir Mix Configuration to Release Configuration and Part 3: Fault Tolerance Doesn’t Come Out Of The Box.