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 - A module we’ve
use
d has imported the function/macro or defined it
Of course there can be a deep hierarchy of import
s and use
s, but they’re all
traceable. We’re left with an explicit system for composing functionality that
is powerful, flexible and easy to reason about.