An opinionated Elixir Style Guide

How to format, structure and keep your Elixir code consistent.

Kamil Lelonek
Kamil Lelonek  - Software Engineer

--

5 best practices in Elixir syntax to improve the readability of your codebase.

Almost every programming language has a style guide introduced by either its creators or the developers working with it. Such a style guide is a set of rules, conventions, and principles which keep code consistent across projects. What is more, it helps to understand the codebase, and lets you quickly navigate through the files. A style guide doesn’t only help to write new modules faster, but also — what is more important — read and reason about business logic easily.

Elixir Stylguide

Elixir doesn’t have an official style guide. It encourages you (via docs) to leverage particular patterns and enforces you (via built-in tools) to follow specific formatting.

For this reason, you don’t have much freedom in designing your code, but, you can still be a bit flexible at some level.

Disclaimer

I’ve been working commercially with Elixir since early 2015. I cooperated with many developers, coordinated multiple teams, and managed various projects. During that time, I created my own set of rules which — I noticed — improved development and programmers’ productivity.

Please keep in mind these are just observations and experiences from my applications in the past years. You may have been working in different teams with different people, and your approach is possibly TOTALLY different.

That’s why I’m open to any other opinion and perspective. Please don't hesitate to leave your comment, especially if you disagree with me.

Rules

Here are my 5 favorite practices I introduce in the teams and projects I work with. Take a glance at them and let me know which one you implement too.

One-liner

The first rule I want to mention is one-liner syntax for simple functions. I noticed many times we are overusing do...end blocks for elementary and short functions.

You can write the same function in two ways:

def encode(file_id, bucket_id) do
enc64("#{file_id}-#{bucket_id}")
end

which is probably your first choice, and:

def encode(file_id, bucket_id), do: enc64("#{file_id}-{bucket_id}")

which is much more succinct.

Sometimes, in case it’s too long, it’s not possible to use one line, but still — two are better than three (especially when you have 10+ of them in one file):

def normalize(%{grid_position: %{x: x, y: y}} = attrs),
do:
%{attrs | grid_position: %{"x" => x, "y" => y}}

When NOT to use it?

Personally, I don’t like following this approach when the contents of the function expands into multiple lines. For example:

defp order_match_suggestion(:order_type, %{order_type: order_type}),
do:
%{
type: :match,
field: :order_type,
value: order_type,
display_name: order_type,
score: score_similarity(order_type)
}

In such case, when the body is multiline, I prefer to wrap it with a do-end block to clarify where exactly the function ends.

The general rule is: use as few lines as possible but be reasonable about it.

do blocks

The second rule is slightly connected with the previous one. It extends it a little to, for example, if, for, with, and cond expressions. Have a look at the following snippet:

if available?(filled_positions, pos) do
[]
else
[grid_position: "a card with this grid position exists"]
end

it can be compressed to something like:

if available?(filled_positions, pos),
do:
[],
else: [grid_position: "a card with this grid position exists"]

3 lines instead of 5. Is it a huge space saver? Not really. But what if you had 10 statements like this? You would save 20 unnecessary lines in your file!

Here are some other examples of such “minification”.

EOF

This one should be obvious, but in 2022 I still see it’s not always respected! That’s why I want to mention it here, even though it’s not related to Elixir exclusively.

Almost all POSIX&UNIX tools require a line ending \n at the end of each file. Many compilers will report a warning otherwise because they will fail unless they find EOF in their inputs. Missing an empty line at the end of a file causes troubles with streaming, appending to, and saving it for later use.

Most of the code editors do that by default but make sure twice you have this option enabled! Even doing a git diff will say a warning "No newline at end of file" if you forget it.

For consistency, make sure to enable a new line at the end of each file.

The other thing worth mentioning by the way is a kind of line ending. We have two, the most common, standards: LF (line feed byte-coded as 0x0A i.e. \n) in Unix and a sequence CRLF (with carriage return byte-coded as 0x0D i.e. \r\n) in Windows. CR (ASCII code 13) comes from the days of typewriters.

OK, enough of this theory. My point is that we should standardize line endings among developers. The best way is to agree on one, common encoding so that the entire repository follows the same ending. I recommend using LF and configuring this in your text editor and git global config:

git config --global core.eol lf
git config --global core.autocrlf false

Piping

Pipe operator |> is one of my favorite elements in Elixir. It introduces the expression on the left-hand side as the first argument to the function call on the right-hand side. And technically, under the hood, it’s just a macro.

The |> operator is mostly useful when you want to execute a series of operations, resembling a pipeline. It embodies one of the main ideas in functional programming, which is the transformation of data, via multiple steps. Unfortunately, from what I’ve noticed, it’s overused in too many places.

Developers, who came to Elixir from other languages, tend to use piping everywhere they can. Even for a single operation when it makes no sense.

There is no benefit in the following call unless you do something afterwards:

"abc" |> String.reverse

You gain nothing by using the pipe here. The code below is much more reasonable, especially when you don’t continue piping later on:

String.reverse("abc")

The rule here is to use the pipe operator only for two or more operations. Do not overuse it for a single function call.

mixing approaches

Yet another pitfall, I see more often than not, is mixing syntaxes when using pipes. Have a look at this code:

keyword["key"]
|> transform1()
|> transform2()
# or ...map.key
|> transform1()
|> transform2()

Do you know what is wrong? In both cases, the first line can be rewritten to use yet another pipe. For the keyword, it could be keyword |> Keyword.get("key") and for the map you can use map |> Map.get(:key).

If you decided on using pipes, keep using them everywhere like that. Do not randomly decide whether to use them or not, avoid mixing syntaxes.

parenthesis

Fortunately, in the recent Elixir versions and mix formatter, parenthesis are now enforced. You have to use them for all function calls, and it’s almost impossible to omit them when you run a formatting task (described in the next section).

However, you have to keep in mind some possible issues related to operator precedence. If you wrote something like this:

iex(1)> String.split "hello word" |> Enum.reverse

You’d see the error like:

** (Protocol.UndefinedError) protocol Enumerable not implemented for "hello word" of type BitString
(elixir 1.13.2) lib/enum.ex:1: Enumerable.impl_for!/1
(elixir 1.13.2) lib/enum.ex:143: Enumerable.reduce/3
(elixir 1.13.2) lib/enum.ex:4144: Enum.reverse/1

The expression would be converted to:

String.split(“hello word” |> Enum.reverse())

which is not what you meant. That’s why my next rule is always to use parenthesis when calling a function. Fortunately, these days, mix formatter will do most of the work for you.

To sum up, the ideal expression may look like this:

“hello word”
|> String.split()
|> Enum.reverse()

or and operators

Last, but not least, are boolean operators. They are symbols and keywords (reserved words) in Elixir that tell the compiler to perform a specific logical operation and produce the final result.

Elixir provides a few of them:

  • and,or and not which are called strict because they only accept booleans and return a boolean result. (They are also short-circuit, so only evaluate the right side if the left one isn’t enough to determine the result).
  • ||, && and ! which are called relaxed, and they operate on falsy (i.e. false and nil) and truthy (anything else, actually) values.
iex(1)> false and raise("This error will never be raised!")
false
iex(2)> false || raise("This error will be raised, tho.")
** (RuntimeError) This error will be raised, tho.

That’s why I strongly recommend using and / or / not syntax when you have something that evaluates to boolean, and you want a boolean result. All other cases will raise BadBooleanError exception then.

Tooling

When you come from another language, especially C#, you are probably familiar with an outstanding refactoring, formatting, and cleanup tool for .NET developers called ReSharper.

If you use JavaScript, you may know Prettier which is widely used across JS codebases, or have ESLint implemented for your repository and as a part of your CI pipeline.

In Elixir, we don’t have many powerful choices compared to the aforementioned communities. There are 2, maybe 3 of them, commonly leveraged. However, I believe we can achieve a lot if we use them wisely.

mix format

mix format is a native and built-in mix task that formats the given .ex(s) files. You don’t have many configuration options there –you can select which files you want to format and choose whether to save files after formatting.

It’s recommended to do this on file save in your text editor or use a key binding. The command is as follows:

cd $project_directory && mix format $file_path

According to StackOverflow Developer Survey in 2021, the most common code editor is Visual Studio Code. Personally, I prefer Atom, but all of them have this option to format files on saving, which is very useful.

You can find installation instructions for elixir formatter here:

credo

Credo is a static code analysis tool for Elixir with a focus on code consistency.

It shows refactoring opportunities in your codebase, complex code fragments, and inconsistencies in your naming.

It can be integrated into most of the common text editors and run directly from the command line as a mix task like mix credo.

It does an initial code review for you and enforces a consistent code style across all the files. Not only that, but it also has powerful configuration options, which you can adjust accordingly to your existing codebase and current needs.

Summary

To sum up, in Elixir we don’t have much flexibility to structure our code. However, there are a few ways to adjust how we write functions, whether we use blocks or which operators we apply in specific cases.

Subscribe to get the latest content immediately
https://tinyletter.com/KamilLelonek

The tools I presented do not enforce many rules natively (e.g., without writing a custom plugin for credo) but they preserve the basic formatting across files.

If you work with a team, I recommend having all rules written down in your favorite documentation tool, on GitHub wiki or in shared documents. It will improve overall collaboration on the project and save time during code reviews. Let me know what your thoughts are and what rules you follow.

--

--