Elixir’s module attributes are used in a few different ways.
First, there are all sorts of useful built-in attributes, like @doc, @moduledoc, @behaviour, etc. For the most part, these are all used for module metadata:
defmodule SomeModule do @behaviour ExampleModule @moduledoc """ Module documentation """ @doc """ Function documentation """ def some_function do ... end end
Second, module attributes can be used as constants:
defmodule SomeModule do @some_constant 1234 def some_function(val) do val / @some_constant end end
Using module attributes as constants, while sometimes useful, can also behave in unexpected ways.
The most important property of module attributes is that they are evaluated at compile time. Which means the following will almost certainly NOT behave correctly in a production environment:
defmodule SomeModule do @some_constant System.get_env("API_KEY") ... end
@some_constant will be assigned the value of of the
API_KEY environment variable during compilation. The
API_KEY environment variable at runtime will then be completely ignored.
Retrieving constants from mix configuration can also cause issues. The following is a popular pattern:
defmodule SomeModule do @some_constant Application.get_env(:some_app, :some_setting) ... end
This seems okay as long as you aren’t changing application settings at runtime using
Application.put_env. However, in some cases (particularly when working with umbrella projects), a change to a configuration file will fail to force a recompile of the module. The module then continues to use the old config value.
This can lead to some really confusing problems that are only solved by doing a full rebuild of the project. This problem is most painful when encountered in a third party library.
Module attributes are also often used to facilitate mocks during testing:
defmodule SomeModule do @some_other_module Application.get_env(:some_app, :other_module, SomeOtherModule) end
This not only suffers from the usual problems with
Application.get_env, but can also make it more difficult to write integration tests and unit tests side-by-side in the test environment.
If a function is used instead of a module attribute, it’s possible to dynamically change the module. Using a mock makes sense during a unit test, but for an integration test you’re likely to want the real thing:
defmodule SomeModule do def some_other_module do Application.get_env(:some_app, :other_module, SomeOtherModule) end end defmodule SomeModuleTest do describe "SomeModule.some_function/0 unit test" do setup do Application.put_env(:some_app, :other_module, MockModule) end test "it works" do assert SomeModuleTest.some_other_module() === MockModule end end describe "SomeModule.some_function/0 integration test" do setup do Application.put_env(:some_app, :other_module, RealModule) end test "it works" do assert SomeModuleTest.some_other_module() === RealModule end end end
In my opinion, it’s best to avoid using module attribute constants outside of simple values. The case could also be made for using them in places where performance is absolutely critical.
Using a module attribute does avoid the overhead of a function call. I’m willing to bet that, in the vast majority of cases, this small performance gain isn’t important.
Send this to a friend