Annotations in Elixir
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 : Functionmodel: 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:
- 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 souse Annotation [:secure, :before]
. - In the
__using__
block setup some annotations to keep the state of the annotations we find i.e.@annotations
- 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. - In the
__before_compile__
provide a methodannotations
to expose the@annotations
attribute.
To see a complete example of this take a look at https://github.com/chrisjowen/Annotatable