Diese Website verwendet Cookies zur Verbesserung des Nutzungserlebnisses sowie für Marketing- und Analysezwecke. Durch Auswahl von “Alle akzeptieren” stimmen Sie der Speicherung von Cookies auf Ihrem Endgerät zu. Details finden Sie in unseren Datenschutzhinweisen.

Engineering

TIL: 3 different ways to build a list with conditional elements in Elixir
6
Minuten Lesezeit
conditional elements in Elixir
Christoph Grothaus

Background: the example

Let’s use the <code>Application</code> module in a standard phoenix app as an example. It contains a list of child processes to be started with the application like this:

children = [
  # Start the Ecto repository
  PhoenixDemo.Repo,
  # Start the Telemetry supervisor
  PhoenixDemoWeb.Telemetry,
  # Start the PubSub system
  {Phoenix.PubSub, name: PhoenixDemo.PubSub},
  # Start the Endpoint (http/https)
  PhoenixDemoWeb.Endpoint
  # Start a worker by calling: PhoenixDemo.Worker.start_link(arg)
  # {PhoenixDemo.Worker, arg}
]

This list is unconditional, all children are always started. After adding some features, I needed to start some children only in some cases, like a task scheduler or an OpenID Connect worker process, which might not be necessary for local development.

First, I attempted to take the standard children list and conditionally concatenate the other children to it. But the order of list elements determines the startup and shutdown order, and the <code>Endpoint</code> entry has to come last, so I needed several sublists. This approach was cumbersome. Finally I came up with a list of possible children, where each is a tuple of <code>{boolean, child_spec}</code>:

possible_children = [
  # Start the Ecto repository
  {true, PhoenixDemo.Repo},
  # Start the Telemetry supervisor
  {true, PhoenixDemoWeb.Telemetry},
  # Start the PubSub system
  {true, {Phoenix.PubSub, name: PhoenixDemo.PubSub}},
  # Start the OpenID Connect Worker for Ueberauth
  {start_oidc_worker?(),
    {OpenIDConnect.Worker, Application.get_env(:ueberauth, Ueberauth.Strategy.OIDC)}},
  # Start the Scheduler
  {start_scheduler?(), DigitalServicesPortal.Scheduler},
  # Start the Endpoint (http/https)
  {true, PhoenixDemoWeb.Endpoint}
  # Start a worker by calling: PhoenixDemo.Worker.start_link(arg)
  # {PhoenixDemo.Worker, arg}
]

That way I can easily specify all child processes in the correct order. The ones that must always be started have a hardcoded <code>true</code> as their first tuple element, the ones that are conditional use private helper functions like <code>start_scheduler</code>? (not shown here) to determine whether they should start.

Now, all I need to do is to transform this list into the final list of children: filter all list elements where the first tuple element is <code>true</code>, and then map all list elements to the second tuple element.

Solutions

There are several ways to achieve this goal. If Elixir just had a <code>filter_map</code> function, like Ruby has! It turns out that at some point in time Elixir did indeed have such a function, but it got deprecated with Elixir version 1.5, in favor of combining other existing functions.

1. Chaining Enum.filter and Enum.map

This one uses filter/2 and map/2 in a straightforward manner:

children =
  possible_children
  |> Enum.filter(fn tuple -> elem(tuple, 0) end)
  |> Enum.map(fn tuple -> elem(tuple, 1) end)

I always feel a little uncomfortable reaching into tuples with elem/2. This solution can also be written in another way by pattern matching on the tuple:

children =
  possible_children
  |> Enum.filter(fn {start?, _} -> start? end)
  |> Enum.map(fn {_, child} -> child end)

And, just to be complete about this, it can also be written with <code>Stream.filter</code> and <code>Stream.map</code>.

2. Using Enum.flat_map and pattern matching on the anonymous function arguments

This one is an unconventional usage of flat_map/2 with a multi-clause pattern match on the anonymous function arguments:

children =
  possible_children
  |> Enum.flat_map(fn
    {true, child} -> [child]
    {false, _} -> []
  end)

3. Using a comprehension

This one relies on a comprehension with a filter:

children = for {start?, child} <- possible_children, start?, do: child

It turns out this can be written even shorter, as the left hand side of the comprehension, the so-called generator expression, supports pattern matching and all non-matching patterns are ignored:

children = for {true, child} <- possible_children, do: child

Conclusion

This was really fun, trying out different ways to build a list with conditional elements in Elixir! Personally, I like option 3 best, it is concise and elegant, and besides it neatly solves my application startup problem.

Sie suchen den richtigen Partner für erfolgreiche Projekte?

Lassen Sie uns ins Gespräch kommen.