Sitemap

Annotations in Elixir

4 min readMay 31, 2017

Elixir has a really interesting feature in the form of module attributes. What makes them interesting is that they are a way of holding state about a module at compile time. In essence they can be thought of as a way to add meta data to a module. Heres a really simple example of this:

defmodule Foo do
@meta “Some meta data”
def meta, do: @meta
end

Some interesting points to note are that the @meta attribute is not available outside of the module and is not changeable after the compilation phase.

One place this compile time state is really useful is in the creation of macros. Macros are also compile time and as such we have no running application or processes. This means that without attributes there is essentially nowhere to hold state. Think of attributes a set of compile time key-value state containers.

Annotations to Attributes

In other languages the @ symbol has a different use case, in Ruby its there for instance variables but in Java, .NET and Typescript they are used to annotate methods and properties. If you are unfamiliar with annotations, they can be thought of as meta data localised to the property or method that directly follows them. Anyone looking at Angular 2 will see that its heavily in use i.e.

@Component({
selector: ‘create-reel’,
templateUrl: ‘./create.reel.component.html’,
})
export class CreateReelFormComponent {
@Input('onAddReel')
addReel : Function
model: any = {}onSubmit() {
.....
}
}

In this case we can *roughly* equate the @Component to elixir attributes in that it gives meta data to the whole class. But what about the @Input annotation, which in this case the information is localised to the addReel property.

In Elixir we have no properties simply modules and methods, but still there exists no way out-of-the-box to localise metadata to the methods. But anyone who has seen the @doc attributes know that this is possible, but how?

The Macro Toolbox

Elixirs macros allow us to basically add any language feature we like to the language.. including making it an OO language :). Defining a basic macro is simply a matter of quoting some code to be essentially injected in place of the calling code i.e.

defmacro hi do
quote do
IO.inspect "Hello"
end
end

Using the hi macro will now replace the calling code with the block inside the quoted region.

While this is great, what we will need to do is have parsed the whole module to track which attributes are directly followed by what methods. If we add the macro at the start of the module then it will have not parsed the module and if we add it at the end then we haven’t started tracking the methods until too late in the execution.

Tool 1: __before_compile__ and __using__

Elixir has us covered here with a handy feature in __before_compile__ to tell the compiler to execute the macro at the point its called but anything inside this quoted block will not appear until the rest of the module is first parsed (this is not exactly what happens buts its close enough to think in these terms).

So with this we can add a macro at the very top of the module, but instead of just calling an unknown macro we can make use of the __using__ macro. Here’s how it looks:

defmodule Annotations do
defmacro __using__(_) do
quote do
@before_compile { unquote(__MODULE__), :__before_compile__ }
end
end
defmacro __before_compile__(env) do
quote do
# Do something after the module is parsed
end
end
end

And we simply use this as follows:

defmodule MyModule do
use Annotations
end

This is great but what do we actually want to do in this __before_compile__ block? Well we need to have tracked all the attributes that have been added to the module and then the methods that follow them, so how can we do this?

Tool 2: __on_definition__

Another great tool in the macro creation toolbox is__on_definition__ which allows us to specify a method that is called every time we encounter a method definition.

This method gives us all kind of information about the method including the env where it was encounter (which includes the module), as well the name, body, guards etc.

Putting it all Together

So at this point we have all the tools we need to add annotations to Elixir methods. The steps to achieve this might be something like:

  1. Use the Annotation module, passing in the names of all attributes we want to treat as annotations i.e. if we wanted to treat the attributes @secure and @before as annotations to methods that follow, we could use the Annotation module like so use Annotation [:secure, :before] .
  2. In the __using__ block setup some annotations to keep the state of the annotations we find i.e. @annotations
  3. In the __on_definition__ block determine the current values of all the attributes we are treating as annotations. Store this with the method information in our previously setup @annotations annotation.
  4. In the __before_compile__ provide a method annotations to expose the @annotations attribute.

To see a complete example of this take a look at https://github.com/chrisjowen/Annotatable

--

--

No responses yet