Cookies
Diese Website verwendet Cookies und ähnliche Technologien für Analyse- und Marketingzwecke. Durch Auswahl von Akzeptieren stimmen Sie der Nutzung zu, alternativ können Sie die Nutzung auch ablehnen. Details zur Verwendung Ihrer Daten finden Sie in unseren Datenschutz­hinweisen, dort können Sie Ihre Einstellungen auch jederzeit anpassen.

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 Application 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 Endpoint 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 {boolean, child_spec}:

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 true as their first tuple element, the ones that are conditional use private helper functions like start_scheduler? (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 true, 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 filter_map 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 Stream.filter and Stream.map.

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.

Are you looking for the right partner for your digital projects?

Let's talk.