Erlang

Elixir Module Attributes - Alchemy 101: Part 1

2016-11-08 by Thomas Hutchinson

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.

First create the file 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, in the next edition we will be looking at what really happens to your mix configuration when you create a release.

Go back to the blog

×

Request more information:

* Denotes required
×

Thank you for your message

We sent you a confirmation email to let you know we received it. One of our colleagues will get in touch shortly.
Have a nice day!