TIL: 3 different ways to build a list with conditional elements in Elixir
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:
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}
:
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:
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:
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:
3. Using a comprehension
This one relies on a comprehension with a filter:
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:
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.