Organize your functional code

German Velasco

When coming from object-oriented languages, I often hear people ask the question, “How do I organize my code? Modules are just bags of functions!”. That is a question I asked myself as well, but after using Elixir and Elm for a while, I have noticed that there is a principle of organization that I keep using and that I see in the wild. I like to think of it as the principle of attraction.

The principle of attraction

Let data attract behavior.

I don’t suppose I invented this, but the intent behind the principle of attraction is that you should organize your modules and functions (behavior) around data structures and abstractions (data). In Elixir, the principle would tell us to organize our modules and functions around Elixir structs, behaviours, and protocols.

Let’s take a look at an example of a popular Elixir library, Plug. Plug is a great library for building web applications. It handles anything (and everything) related to the request and response. If you come from the Ruby world, Plug will remind you of Rack.

At the core of Plug are two things, a connection struct called Plug.Conn and a specification for what exactly is a plug. Once you notice those two things, you will see that all modules and functions revolve around one of those two things.

Plug.Conn

First, note that all functions within the Plug.Conn module are related to a connection struct, which is defined inside that very module. But if you take a closer look, you will also see how each of those functions takes in a connection struct.

Here’s a list of a few functions defined in Plug.Conn,

  • assign(conn, key, value)
  • clear_session(conn)
  • put_resp_content_type(conn, content_type, charset)
  • put_resp_header(conn, key, value)
  • put_session(conn, key, value)
  • send_resp(conn, status, body)

Notice a pattern? They all take conn, the connection struct, as their first argument, and though you can’t see it, they all return a (modified) connection struct.

They do that because this module revolves around the Plug.Conn struct. And passing the connection struct as the first argument allows for composition via pipelines.

Take a look at this example for interacting with a request,

conn
|> put_session(:current_user, user)
|> put_resp_header(location, "/")
|> put_resp_content_type("text/html")
|> send_resp(302, "You are being redirected")

Very clean, right?

So, the first way to apply the principle of attraction is to organize your modules and functions around the struct that they are modifying. The struct attracts the functions.

Plug as a specification

The second way in which Plug is organized is around the notion of a plug. That may sound like a tautology, but what I mean is that many Plug modules make use of a plug or are themselves valid plugs (that is they follow the Plug specification).

What is the Plug specification? Glad you asked.

In order for a module to be a valid plug, it must define an init/1 function that takes a set of options and returns a set of options, and it must define a call/2 function that takes a connection struct as its first argument, the set of options as its second one, and it must return a connection struct. A function plug simply has to abide by the same specification as the call/2 function in the module plug, taking the connection struct and the options as arguments, and returning a connection struct.

This is an example of a valid module plug,

defmodule CustomPlug do
  def init(opts) do
    opts
  end

  def call(conn, _opts) do
    conn
  end
end

Now let’s take a look at how Plug, the library, builds modules around the notion of plugs.

Plug.Builder is a module in the Plug library that allows us to define a pipeline of plugs that will be executed sequentially in the order in which they are defined. The magic really comes in when many modules in the Plug library are plugs themselves, and can be thus used in such a pipeline.

Let’s look at an example,

defmodule MyPlugPipeline do
  use Plug.Builder

  plug Plug.Logger
  plug Plug.RequestId
  plug Plug.Head
  plug :hello

  def hello(conn, _) do
    Plug.Conn.send_resp(conn, 200, "Hello world!")
  end
end

Note how Plug.Logger, Plug.RequestId, and Plug.Head are themselves plugs and for that reason can be used in the pipeline provided by Plug.Builder. We can also mix and match by defining our own function plug within the module (the plug :hello part).

By organizing modules and functions around the plug abstraction, Plug.Builder allows us to compose rich pipelines with other modules and functions that satisfy the abstraction.

What next?

I certainly think there are other principles by which to organize your functional code, but I do recommend keeping this one handy. I have found it to be quite useful! It even works when modules are nested within other modules. For example, Plug.Conn does not have all the logic related to a connection struct in itself. Sometimes it uses other modules that are namespaced under Plug.Conn like Plug.Conn.Status. But if you look closely, Plug.Conn.Status also uses the principle of attraction by having all of its functions deal with the same piece of data, a status map defined inside the module.