Don’t Do This: Object Oriented Inheritance in Elixir with Macros

Don’t Do This: Object Oriented Inheritance in Elixir with Macros
Photo by Mario Mesaglio / Unsplash

Elixir's macro system lets developers do incredible things — including some you probably shouldn’t

A recent post in the Elixir forum of Reddit asked how Elixir accomplishes code reuse without inheritance. Coming from an object-oriented background, the poster was used to solving this problem by inheriting behaviors with classes, but wasn’t sure how to approach it in a functional language like Elixir.

The general answer is that Elixir favors a composition approach, constructing complex behaviors by importing functionality from other modules. But the post got me thinking: even though it might be a terrible thing to do, is it possible to implement classic object-oriented style inheritance in Elixir?

It turns out, yes: Elixir’s macro system is powerful enough to implement object-oriented style inheritance with modules. In this post, we’ll learn about the Elixir macro system and how to (ab)use it to implemenent inheritence. We’ll also talk a bit about why it’s probably not a good idea to do so.

Macros & Metaprogramming

Metaprogramming, broadly speaking, is the ability to manipulate code as data. This means that a programming language can read, modify and generate its own code, and it’s just as powerful and dangerous as it sounds.

Elixir enables metaprogramming via its macro system. The macro system lets us write and execute code that generates code, and this opens the door to all kinds of language extensions and mechanisms for code reuse. Let’s see a simple example, where we have some “mixin” functions we want to inject into a module:

defmodule Mixin do
  defmacro __using__(_) do
    quote do
      def mixin_func1(value), do: IO.inspect(value)
      def mixin_func2(value), do: IO.inspect(value * 2)
    end  
  end
end

# add the "use" macro to inject functions into MyModule
defmodule MyModule do  
  use Mixin
end

MyModule.mixin_func2(5)
> 10

Instead of simply executing code, macros can return code that can be executed by the caller–including code that is generated dynamically. This is done using a quote do ... end block to quote the code and return its data representation rather than executing it. In this simple use case, the __using__ macro lets us effectively inject our function definition code into MyModule with use Mixin.

For a more real-world example, consider this piece of code generated in a default project in the Phoenix Framework (with some modifications). In this example, controller modules like MyAppWeb.UserController need to have access to certain functionality. Instead of subclassing some base controller class, we use a macro to inject the desired code:

# our project's macros
defmodule MyAppWeb do
  def controller do
    quote do
      use Phoenix.Controller, namespace: MyAppWeb
      import Phoenix.LiveView.Controller
      import Plug.Conn
      import MyAppWeb.Gettext
      alias MyAppWeb.Router.Helpers, as: Routes
    end
  end

  defmacro __using__(which) when is_atom(which) do
    apply(__MODULE__, which, [])
  end
end

# our controller implementation
defmodule MyAppWeb.UserController do
  use MyAppWeb, :controller
end

From a code reuse standpoint, metaprogramming lets us accomplish the same sorts of things we do with inheritance — as well as other forms of code reuse that don’t neatly fit the inheritance model.  Note that this example not only imports some functionality, it also calls another use.

But macros go far beyond simple code reuse: in addition to injecting code with use, macros can be used to construct control structures or domain specific language APIs (DSLs) . Here’s an API example from the popular Ecto database mapper library:

import Ecto.Query, only: [from: 2]

query = from u in "users",
          where: u.age > 18,
          select: u.name

Repo.all(query)

Ecto uses a macro definition for from,  instead of a function, so that API users can write expressions like u.age > 18 which can be transformed into database queries rather than being evaluated directly. In a function call, the expression is evaluated as usual and the function receives the value. In a macro, the expression is provided as code and can be read, manipulated or executed as the macro author chooses. In this case, the expression is transformed into a SQL database query fragment rather than being executed in Elixir.

To dig deeper on this, let’s take a look at an example from the Elixir documentation. In the following example, a macro is used to generate a new control structure called Unless. The example shows one implementation using a function and one using a macro, to demonstrate the difference:

defmodule Unless do
  def fun_unless(clause, do: expression) do
    if(!clause, do: expression)
  end

  defmacro macro_unless(clause, do: expression) do
    quote do
      if(!unquote(clause), do: unquote(expression))
    end
  end
end

In the function example, the expression is evaluated immediately as part of the normal program execution. But by using a macro, the expression is injected into a control structure and only executed when the clause is false–which is the behavior we expect from a flow control structure!

iex> require Unless
iex> Unless.macro_unless true, do: IO.puts "this should never be printed"
nil
iex> Unless.fun_unless true, do: IO.puts "this should never be printed"
"this should never be printed"
nil

The Unless macro uses a quote do … end block like our previous example, but also uses unquote to inject the passed in expressions into the quoted code block.

While metaprogramming is extremely powerful, it can be “dangerous” in the sense that it can modify expected code behaviors and make code extremely difficult to understand if used incorrectly. As the Elixir documentation notes:

Macros should only be used as a last resort. Remember that explicit is better than implicit. Clear code is better than concise code

Writing macros is somewhat uncommon for day-to-day Elixir development, with the exception of some common and well-established use cases like the __using__ example above.

Building a Macro for Object-Oriented Style Inheritance

Now that we’ve seen how macros can be used in Elixir to extend module behavior, let’s go back to our original question: can we use Elixir macros to implement OO-style inheritance? We can, but it will require a much more complicated macro!

First, let’s state the goals of our inheritance macro:

  • We want to implement an “Inherit” macro which will take a parent module, and inject all of the functions from the parent module into the current module.
  • It should work with multilevel inheritance so that if C inherits from B, and B inherits from A, then C should be able to call A functions as well.
  • Like most OO languages, functions should be overridable: we should allow inherited functions to be overridden and reimplemented
  • Finally we should support multiple inheritance: the ability to inherit from separate base classes and get functions from both.

If we implement our inherit macro correctly, we should be able to write code like the following:

defmodule Base do
  def f1(a), do: a * 1
  def f2(a), do: a * 2
end

defmodule Base2 do
  def f3(a), do: a * 3
end

defmodule Derived do
  use Inherit, Base
end

defmodule MyModule do
  use Inherit, Derived
  use Inherit, Base2

  def f2(a), do: a * 10
end

# Call an inherited function
MyModule.f1(a) |> IO.inspect()
> 5

# Call an overridden function
MyModule.f2(a) |> IO.inspect()
> 50

# Call a function inherited with multiple-inheritance
MyModule.f3(5) |> IO.inspect()
> 15

After some trial and error to solve a couple of tricky problems described below, I was able to come up with a macro which passed all the tests:

defmodule Inherit do
  defmacro __using__(quoted_module) do
    module = Macro.expand(quoted_module, __ENV__)

    module.__info__(:functions) |> Enum.map(fn {name, arity} ->
      # Generate an argument list of length `arity`
      args = arity== 0 && [] || 1..arity |> Enum.map(&Macro.var(:"arg#{&1}", nil))

      quote do
        defdelegate unquote(name)(unquote_splicing(args)), to: unquote(module)
        defoverridable [{unquote(name), unquote(arity)}]
      end
    end)
  end
end

What we’re doing in this macro is

  • implement a macro which takes in a base module to inherit from with defmacro — note that we need to get the unquoted module with Macro.expand
  • iterate over all of the functions the module exposes using module.__info__(:functions)
  • use defdelegate to define a function in which points at the base implementation — this was the trickiest part and is described in more detail below.
  • use defoverridable to allow the function to be override in the derived class if desired

As mentioned above, the tricky part of this macro is constructing the defdelegate call, specifically generating a delegate definition with the correct number of arguments. For example, if we need to define a method do_stuff with 3 arugments, we need to generate code that looks like this:

defdelegate do_stuff(a1, a2, a3), to: unquote(module)

It’s simple enough to can construct a list of argument variables, but how do we put it into the function definition? As it turns out, a simple unquote won’t solve our problem here — that would essentially be a function definition with a single list argument. Instead, we use a function called unquote_splicing: it unquotes a list, expanding the elements in place. Let’s take a quick look at a simple unquote vs. unquote_splicing example:

iex(6)> quote do [1, unquote(list), 2] end
[1, [2, 3], 2]
iex(7)> quote do [1, unquote_splicing(list), 2] end
[1, 2, 3, 2]

With this trick in place to expand the arguments, we implement the function definition block as follows:

quote do
  defdelegate unquote(name)(unquote_splicing(args)), to: unquote(module)
  defoverridable [{unquote(name), unquote(arity)}]
end

And voila!  In just a couple of lines, we've implemented OO-style inheritence in Elixir.

Conclusion: Seriously, Don’t Do This

Implementing object oriented style inheritance in Elixir is a great little exercise to learn a bit more about the macro system, but it’s probably not a good idea to use it in an Elixir application. While inheritance is a common tool in other languages, it’s not a common Elixir idiom and is likely to cause confusion (and probably well-deserved scorn) if used in an Elixir codebase.

That said, the power of the macro system is that we can implement these kinds of specialized structures when necessary. Though I can’t imagine using a macro like this for every day application development, it’s certainly possible that functionality like this could be used for a specialized library or DSL, similar to Ecto’s specialized use of macros.

To learn more about metaprogramming in Elixir, I recommend the book Metaprogramming Elixir: Write Less Code, Get More Done (and Have Fun!) by Phoenix Framework author Chris McCord.