April 03, 2015

The Value of Explicitness

Elixir is often compared to Ruby. It’s true that Elixir takes inspiration from Ruby’s syntax. One of Elixir’s core tenets is “metaprogramming”, something often associated with Ruby and its ecosystem. That said, after using Elixir for a small amount of time it is immediately obvious that the semantics of the languages are very different. More subtly, Elixir focuses on explicitness where Ruby is often implicit. In this blog post, I’ll discuss Elixir’s approach to composing functionality in an explicit manner.

Building Blocks

The building blocks of Elixir applications are straightforward; modules, functions and macros. Modules are simply a namespaced bucket where functions or macros are defined.

defmodule Foo do def add ( a , b ) do a + b end end

We can call functions in a module by qualifying the function with the module name.

Foo . add ( 1 , 2 ) # => 3

This is true within other modules as well.

defmodule Bar do def add ( a , b , c ) do x = Foo . add ( a , b ) Foo . add ( x , c ) end end Bar . add ( 1 , 2 , 3 ) # => 6

Functions within a module can make unqualified calls to other functions within that same module.

defmodule Bar do def a do 1 end def b do a + 2 end end Bar . b # => 3

If we’d like to use macros from module Foo within Bar , we must first require it.

defmodule Foo do defmacro greeter ( message ) do quote do def greet () do unquote ( message ) end end end end defmodule Bar do require Foo Foo . greeter ( "hello, world!" ) end Bar . greet # => "hello, world!"

Reducing Verbosity

If our module Bar is making heavy use of functions/macros in Foo , it can be tedious to always qualify those calls. import solves this problem.

defmodule Foo do defmacro greeter ( message ) do quote do def greet () do unquote ( message ) end end end def first do 1 end def second do 2 end end defmodule Bar do import Foo greeter ( "hello, world!" ) def sum do first + second end end Bar . greet # => "hello, world!" Bar . sum # => 3 Bar . first # => this will error

Note that while we can now make unqualified calls to functions to Foo inside Bar , the import is explicit. It is also important to recognize that Bar does not expose Foo ’s functions externally even though we’ve import ed it.

We can also import a subset of functions from Foo by providing of list of function names and arities.

defmodule Bar do import Foo , only: [ first: 0 ] def increment do first + 1 end end Bar . increment # => 2

More Flexibility

There are cases where we’d like to give module Foo the ability to inline some code into our module Bar . This would let us do things like setup DSLs, abstract more complicated import graphs, etc.

As an example, suppose we want to be able to write code like this:

defmodule Add do def add ( a , b ) do a + b end end defmodule Multiply do def multiply ( a , b ) do a * b end end defmodule Rectangle do import Add import Multiply def area ( a , b ) do multiply ( a , b ) end def perimeter ( a , b ) do multiply ( add ( a , b ), 2 ) end end Rectangle . area ( 2 , 3 ) # => 6 Rectangle . perimeter ( 2 , 3 ) # => 10

This works, but it’s a pain to import both the Add and Multiply modules. Let’s make a Math module that does the hard work for us.

defmodule Math do defmacro import_deps do quote do import Add import Multiply end end end defmodule Rectangle do require Math Math . import_deps def area ( a , b ) do multiply ( a , b ) end def perimeter ( a , b ) do multiply ( add ( a , b ), 2 ) end end

Great! We’ve essentially inlined the code in Math.import_deps into our Rectangle module, but it’s all still explicit. This pattern is common enough that Elixir introduces use . use will require the provided module and then call the __using__ macro. Let’s rewrite the code above.

defmodule Math do defmacro __using__ ( _opts ) do quote do import Add import Multiply end end end defmodule Rectangle do use Math def area ( a , b ) do multiply ( a , b ) end def perimeter ( a , b ) do multiply ( add ( a , b ), 2 ) end end

It’s important to stop and recognize how powerful this pattern is. Because __using__ is a macro, we have a lot of flexibility. We can import other modules, call functions, call macros, etc. And yet, it’s all explicit.

Wrapping Up

Given what we now know, any time we see an unqualified function/macro call in a module, one of the following is true:

The function/macro is defined in the same module

The function/macro has been import ed from another module

ed from another module A module we’ve use d has imported the function/macro or defined it