Yos Riady software craftsman 🌱

Mixins in Elixir

Mixins in Elixir

Let’s demystify the frequently used use keyword in Elixir.

We can use the use keyword in Elixir to package behaviour and implementation for use in other modules, similar to abstract base classes or mixins in other languages. Mixins reduce the amount of boilerplate in your modules, especially for library authors.

Mixins in Elixir are similar on the surface to Concerns in Ruby and Traits in Scala. Before we start, you should be somewhat familiar with behaviours and macros.

The use keyword

use requires the given module and calls the __using__ macro defined for that module. To illustrate, both examples below are equivalent:

defmodule MyModule do
  use Utils, argument: :value
end
defmodule MyModule do
  require Utils
  Utils.__using__(argument: :value)
end

Example

Now take a look at the Filter module defined below:

defmodule Filter do
   # Define behaviours which user modules have to implement, with type annotations
   @callback transform(String.t) :: String.t

  # When you call use in your module, the __using__ macro is called.
  defmacro __using__(_params) do
    quote do
      # User modules must implement the Filter callbacks
      @behaviour Filter

      # Define implementation for user modules to use
      def shout, do: IO.puts("HEY!")
      def greet(s), do: IO.puts(s)

      # Defoverridable makes the given functions in the current module overridable
      # Without defoverridable, new definitions of greet will not be picked up
      defoverridable [shout: 0, greet: 1]
    end
  end
end

First, we define a behaviour callback transform with the @callback attribute. Modules that use the Filter module will need to implement transform.

We then define the __using__ macro using defmacro. __using__ is called whenever the use keyword is called with this module.

We can accept any _params specified in the use call, but we’re not using any for this example.

Within the body of the __using__ macro, we use quote to return a quoted expression. The execution of this quoted expression is deferred until runtime, when the use keyword is executed within the user module. This lets us inject code to user modules.

Read this if you need a refresher on macros.

Here we use the @behaviour attribute to denote that modules that use Filter must implement the callbacks defined in Filter.

We define some default concrete implementations of shout/0 and greet/1 which will be injected to user modules. We then use defoverridable to denote that it can be overriden.

Now let’s take a look at a MyFilter module which use the Filter module:

defmodule MyFilter do
  use Filter

  # Override default Filter implementation
  def greet(s), do: IO.puts("Sup " <> s)
end

In our module we override the greet/1 function with our own implementation.

If we didn’t explicitly add greet/1 to our defoverridable, new definitions of greet/1 will not be picked up and we would use the original definition instead.

However, compiling the above module gives us the following warning: warning: undefined behaviour function transform/1 (for behaviour Filter). This is because we’re missing a required behaviour implementation transform. Let’s implement all our behaviour callbacks:

defmodule MyFilter do
  use Filter

  def transform(s), do: s

  # Override default Filter implementation
  def greet(s), do: IO.puts("Sup" <> s)
end

MyFilter will now compile without warnings. With just a single line of code, we’ve injected MyFilter with behaviours and default overridable implementation:

iex> MyFilter.transform "yay"
yay
:ok
iex> MyFilter.shout
HEY!
:ok
iex> MyFilter.greet "Yos"
Sup Yos
:ok

Other Examples

If you’ve played around with the OTP, you would have come across use GenServer. The GenServer module uses use to give developers a set of default client-server functionality which can be overriden. Here is an excerpt of its __using__ macro:

  defmacro __using__(_) do
    quote location: :keep do
      @behaviour :gen_server

      @doc false
      def init(args) do
        {:ok, args}
      end

      @doc false
      def handle_call(msg, _from, state) do
        # We do this to trick Dialyzer to not complain about non-local returns.
        reason = {:bad_call, msg}
        case :erlang.phash2(1, 1) do
          0 -> exit(reason)
          1 -> {:stop, reason, state}
        end
      end

      @doc false
      def handle_info(_msg, state) do
        {:noreply, state}
      end

      @doc false
      def handle_cast(msg, state) do
        # We do this to trick Dialyzer to not complain about non-local returns.
        reason = {:bad_cast, msg}
        case :erlang.phash2(1, 1) do
          0 -> exit(reason)
          1 -> {:stop, reason, state}
        end
      end

      @doc false
      def terminate(_reason, _state) do
        :ok
      end

      @doc false
      def code_change(_old, state, _extra) do
        {:ok, state}
      end

      defoverridable [init: 1, handle_call: 3, handle_info: 2,
                      handle_cast: 2, terminate: 2, code_change: 3]
    end
  end

Alternatives to use

If you don’t need metaprogramming, control over method overriding, nor pass in parameters to your use call - look into using import instead.

In Closing

Mixins are a powerful way to organize your modules that is well worth its place in your Elixir toolbox. I hope I have helped you dispel the magic of use. You should have a better understanding on how you can use mixins to inject code into other modules and reduce boilerplate.

Author

Yos is a software craftsman based in Singapore.

📬 Subscribe to my newsletter

Get notified of my latest articles by providing your email below.


Going Serverless book

Interested to find out more about serverless? Going Serverless teaches you how to build scalable applications with the Serverless framework and AWS Lambda. You'll learn how to design, develop, test, deploy, and secure Serverless applications from planning to production.

Learn More →