Using Control Flow in Elixir to Improve Discoverability

Photo by Adam Jang on Unsplash

At Podium we focus on writing discoverable code. Code is read many more times than it is written, so it is important to us that our code is understandable. With all the different ways to branch control flow in Elixir, we’ve come to realize that the method you choose can convey meaning to the reader. Here are a couple of guidelines we tend to follow in order to boost the discoverability and convey purpose to the reader.

IF

With all the more flashy ways to branch in Elixir, if statements are often left under-utilized. However, if statements are the most universally familiar type of control flow, and almost every language implements them. Given that, we try to use them frequently. Our guideline has been, when branching on a condition that does not benefit from pattern matching, and you only have one or two possible outcomes, use an if statement.

Example:

CASE

case statements are typically the go-to conditional when a pattern match leads to two or more branches. The most common use case for us is matching status tuples like {:ok, “success"} and {:error, “failure"}. These types of branches around static pattern matching are quick for a reader to pick up on, and they support a lot of flexibility for the writer.

Example:

COND

This one is typically not used often, but it may come up if you have many conditional expressions you’d like to check against.

Example:

WITH

Here at Podium, we’ve been writing in elixir since before with came into existence. We were thrilled to see this new control flow option land. Before with we had a lot of code that contained nested branching case statements, that quickly became hard to follow. with is great at describing a control flow that contains multiple steps and branching. That said, if you are not too careful, with statements can become overly complex and hard to follow. We try to limit our branching logic inside a with statement to help reduce that complexity. Once a with statement reaches more than three or four branches, we consider further abstraction of the behavior.

Another thing to be careful about is ensuring your with statement starts and ends with <-. You can put assignments in the middle, but they are only necessary if you’d like to bind the variable inside the with statement. Take this example

This can be rewritten like this:

Here is a real world example of a with statement we refactored to use if statements instead. The code below takes a changeset that may contain an image_url. If that image url is a url that is cached in our cdn, then we update the image_url to the cdn location and set the image_source_url to the original url. Here is the original code that was written with the with statement:

Here is the same behavior refactored to improve the readability of the code:

After exploring both options for this behavior, we found the second to be the most discoverable for future readers.

We have also found with statements to be useful for returning meaningful errors, and executing code after we know something has been successful. If you are familiar with Ruby, it feels similar to the tap method, but the “block” in this case conditionally executes. A great use case for example would be writing something to the database and then if it was successful publish the new resource to any clients subscribed on a websocket.

Example:

A nice benefit of the above pattern is that when an error occurs, it is returned from the with statement. We have established a useful pattern around this: when an error occurs, we return an error tuple with a changeset. Our controllers then handle rendering the invalid changeset as an error response.

Functional pattern matching

As a unique property of Elixir, pattern matching at the function level tailors functions to specific concerns, making certain functions more clear and readable. This control flow can become less discoverable when you have a lot of pattern matching happening or many branches of logic. When we use this technique here at Podium, we try to limit the pattern matching to specific cases like base cases in recursion. We find that by adhering to this guideline we are able to keep our functions focused but also readable.

Example:

In Conclusion

From the beginning of our Elixir journey here at Podium, we’ve found the language to be expressive and a true joy to work in on a daily basis. When it comes to control flow and branching logic, we’ve had a lot of great opportunities to iterate on best practices, maximizing the readability and discoverability of our code. The net result of this has been increased velocity for our engineering team. As mentioned at the beginning of this article, code is read many more times than it is written, and hopefully this synopsis on Elixir control flow and our learnings over time can help other teams decide how to structure their code in the future.