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 imported 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 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 imports and 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.