This post is mostly a tutorial on Elixir macros, yet by the end we’ll have a helper module that supports bound variables passed to defmodule built.

The problem

Nested modules in Elixir derive the “context” if one treats context as fully name qualification. In all other aspects, these two codepieces are equivalent:

defmodule A . B do end # versus defmodule A do defmodule B do end # the only difference is that here # we can refer B module as B end

Usually when one nests anything, they expect to derive more context. Like module variables, or whatever. Even more, the context switch should probably allow passing a binding through. At least, quote macro does:

block = quote bind_quoted: [ var: 42 ] do var == 42 end Code . eval_quoted block #⇒ {true, [{\{:var, Elixir}, 42}]}

The above trick is specifically handy when writing macros:

defmodule A do defmacro a ( var ) do quote bind_quoted: [ var: var ] do IO . puts var # use it as is, without `unquote` end end end require A A . a ( 42 ) #⇒ 42

OK, so we are to implement binding passing through for defmodule . The code that might use our implementation, would look like:

defmodule WithBinding do # our module, that implements custom `defmodule` macro require Atadura Atadura . defmodule DynamicModule , status: :ok , message: "¡Yay!" do def response , do : [ status: status , message: message ] IO . inspect message , label: "Message (local)" #⇒ Message (local): "¡Yay!" IO . inspect @message , label: "Message (attribute)" #⇒ Message (attribute): "¡Yay!" IO . inspect ~b|status message| , label: "Message (sigil)" #⇒ Message (sigil): [:ok, "¡Yay!"] end end WithBinding . DynamicModule . response () #⇒ [status: :ok, message: "¡Yay!"]

As one might see, both status and message variables were bound to newly created module in three different ways:

— as local functions;

— as module variables;

— as smart sigil.

Also the module was granted with bindings/0 function, returning the bindings keyword list as is.

Under the hood, the nested module WithBinding.DynamicModule.Bindings was also created, having two interesting macros, bindings!/0 and attributes!/0 . The former populates the binding as local variables, the latter declares modules attributes (it is internally used to declare module attributes for the binding in the example above.)

bindings!/0

require WithBinding . DynamicModule . Bindings WithBinding . DynamicModule . Bindings . bindings! #⇒ [:ok, "¡Yay!"] status #⇒ :ok

attributes!/0

defmodule A do require WithBinding . DynamicModule . Bindings WithBinding . DynamicModule . Bindings . attributes! def status , do : @status end A . status #⇒ :ok

Implementation

The implementation is simple, though it might look a bit cumbersome at the first glance. Let’s start with Atadura.defmodule/{2,3} .

If no bindings were given, Atadura.defmodule/2 would gracefully fallback to Kernel.defmodule/2 , without any impact on the compiled code.

Here is the complete code of this module, including some comments:

# function declaration; needed to allow importing w/out # conflicts with `Kernel.defmodule/2` and fallback to it # when no parameters are given. defmacro defmodule ( name , bindings \\ [], do_block ) # fallback to `Kernel.defmodule/2` defmacro defmodule ( name , [], do : block ) do quote do : Kernel . defmodule ( unquote ( name ), do : unquote ( block )) end # handler for non-bracketed call (not available on import) defmacro defmodule ( name , [], bindings_and_do ) do block = Keyword . get ( bindings_and_do , :do , nil ) bindings = Keyword . delete ( bindings_and_do , :do ) quote do : Atadura . defmodule ( unquote ( name ), unquote ( bindings ), do : unquote ( block )) end # main handler defmacro defmodule ( name , bindings , do : block ) do # we are to build the module name in the first place # it’s done before `quote do` because `defmodule` below # does not accept `Module.concat(unquote(name), Bindings)` binding_module = with { :__aliases__ , line , names } <- name do { :__aliases__ , line , names ++ [ :Bindings ]} end quote do # `Bindings` nested module, passes `bindings` to # `Atadura.Binder`, that will declare all the # stuff in `__using__` callback defmodule unquote ( binding_module ) do use Atadura . Binder , unquote ( bindings ) end # OK, that is the exact reason we were quoting # and unquoting everything: call to `use binding_module` # is required to update (patch, extend, younameit) the # module that is being created defmodule unquote ( name ) do use unquote ( binding_module ) unquote ( block ) end end end

That’s almost it. The only thing remains is to implement __using__ there in Atadura.Binder . Since it’s a bit of hardcore, let’s continue with usage advises on that module. The post’s tail will cover the tech details of the implementation, I promise.

Tips and tricks

import Atadura , only: [ :defmodule , 3 ]

The above allows to still use default Kernel.defmodule/2 unless the keyword list is given as the second parameter:

import Atadura , only: [ :defmodule , 3 ] # Plain old good `Kernel.defmodule/2` without bindings defmodule A1 , do : def a , do : IO . puts "★★★" # `Atadura.defmodule/3` with bindings defmodule A2 , [ b: 42 ], do : def a , do : IO . puts "★★★"

Without explicit import , Atadura.defmodule/{2,3} would gracefully fallback to Kernel.defmodule/2 if no bindings were given. Unfortunately, there is no chance to distinguish between two following calls:

defmodule A1 , do : def a , do : IO . puts "★⚐★" defmodule A2 , bound: 3.14 , do : def a , do : IO . puts "★π★"

because both examples are defmodule/2 clauses, and we would be conflicting with always imported Kernel . So, when import Atadura is used, the brackets around the second argument in call to defmodule are mandatory.

Atadura.Binder.__using__/1

Dear José, thanks for still bearing with me, in case you wonder how ugly I had this written, here are the implementation details.

defmodule Atadura . Binder do @moduledoc ~S"" " Since we want to hide the implementation details inside `Bindings` nested module, we’ll do nested `__using__`. See `Atadura.defmodule/3` for details. """ defmacro __using__ ( bindings ) do [ quote do # bindings require two unquotes, on each subsequent quote bindings = unquote ( bindings ) # this will be used in nested `Bindings` module, that will # be in turn `use`d by `Atadura.defmodule/3` defmacro __using__ ( _opts \\ []) do module = __MODULE__ # to get it properly inside quote quote do require unquote ( module ), as: Bindings import unquote ( module ) # Bindings.bindings! # declare local variables: not very handy Bindings . attributes! # declare module attributes # just an example of how module documentation # might be constructed on the fly Module . add_doc ( __MODULE__ , 0 , :def , { :version , 0 }, [], ~s"" " Returns a binding for this module, supplied when it was created. This module #{ __MODULE__ } was created with the following binding: #{ inspect Bindings . bindings } Enjoy! """ ) # the plain function, that returns all the bindings def bindings , do : Bindings . bindings end end # back to `Bindings` module level, here it’s macro called 3 LOCs above defmacro bindings , do : unquote ( bindings ) # local variables producer, might be called from everywhere # to populate the current context with binding defmacro bindings! do Enum . map ( unquote ( bindings ), fn { attr , val } -> {: = , [], [{ attr , [], nil }, val ]} end ) end # module attribute producer, might be called from inside # of any module to populate the current context with binding defmacro attributes! () do Enum . map ( unquote ( bindings ), fn { attr , val } -> quote do Module . register_attribute ( __MODULE__ , unquote ( attr ), accumulate: false ) Module . put_attribute ( __MODULE__ , unquote ( attr ), unquote ( val )) end end ) end # sigils producer; when called with a single attribute name, # e.g. `~b|status|`, returns it’s value as is. # when called with many, returns a list of values. defmacro sigil_b ( keys , _modifiers ) do bindings = unquote ( bindings ) { :< <>> , _ , [ key ]} = keys case String . split ( key , ~r/\s+/ ) do [ key ] when is_binary ( key ) -> # single value requested quote do : unquote ( bindings )[ String . to_atom ( unquote ( key ))] keys when is_list ( keys ) -> # multi values Enum . map ( keys , fn key -> with bindings <- unquote ( bindings ), do : quote do : unquote ( bindings )[ String . to_atom ( unquote ( key ))] end ) end end end | # functions returning bindings by name, appended to the AST. Enum . map ( bindings , fn { attr , val } -> quote do def unquote ( attr )(), do : unquote ( val ) end end ) ] end end

Yes, that is it. There is no more code in the package, besides the above, but for those picky persons, we have atadura @ github, also the package is available @ hex.pm.