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.
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
Bar, we must first
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!"
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
import is explicit. It is also important to recognize that
does not expose
Foo’s functions externally even though we’ve
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
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
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 will require the provided module and then
__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.
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
imported from another module
- A module we’ve
used has imported the function/macro or defined it
Of course there can be a deep hierarchy of
uses, but they’re all
traceable. We’re left with an explicit system for composing functionality that
is powerful, flexible and easy to reason about.