Elixir Style Guide
A programmer does not primarily write code; rather, he primarily writes to another programmer about his problem solution. The understanding of this fact is the final step in his maturation as technician.
— What a Programmer Does, 1967
Table of Contents
Source Code Layout
-
Use two spaces per indentation level. No hard tabs. [link]
-
Use one expression per line, as a corollary - don't use semicolon
;
to separate statements and expressions. [link] -
Use spaces around binary operators, after commas
,
, colons:
and semicolons;
. Do not put spaces around matched pairs like brackets[]
, braces{}
, etc. Whitespace might be (mostly) irrelevant to the Elixir compiler, but its proper use is the key to writing easily readable code. [link]sum = 1 2 [first | rest] = 'three' {a1, a2} = {2, 3} Enum.join(["one", <<"two">>, sum])
-
No spaces after unary operators and inside range literals, the only exception is the
not
operator. [link]angle = -45 ^result = Float.parse("42.01") 2 in 1..5 not File.exists?(path)
-
Use spaces around default arguments
\\
definition. [link] -
Do not put spaces around segment options definition in bitstrings. [link]
# Bad <<102 :: unsigned-big-integer, rest :: binary>> <<102::unsigned - big - integer, rest::binary>> # Good <<102::unsigned-big-integer, rest::binary>>
-
Indent
when
guard clauses on the same level as the function/macro signature in the definition they're part of. Do this only if you cannot fit thewhen
guard on the same line as the definition. [link]def format_error({exception, stacktrace}) when is_list(stacktrace) and stacktrace != [] do # ... end defmacro dngettext(domain, msgid, msgid_plural, count) when is_binary(msgid) and is_binary(msgid_plural) do # ... end
-
When assigning the result of a multi-line expression, do not preserve alignment of its parts. [link]
# Bad {found, not_found} = Enum.map(files, &Path.expand(&1, path)) |> Enum.partition(&File.exists?/1) prefix = case base do :binary -> "0b" :octal -> "0o" :hex -> "0x" end # Good {found, not_found} = Enum.map(files, &Path.expand(&1, path)) |> Enum.partition(&File.exists?/1) prefix = case base do :binary -> "0b" :octal -> "0o" :hex -> "0x" end
-
Add underscores to large numeric literals to improve their readability. [link]
num = 1_000_000
-
When using atom literals that need to be quoted because they contain characters that are invalid in atoms (such as
:"foo-bar"
), use double quotes around the atom name: [link]# Bad :'foo-bar' :'atom number #{index}' # Good :"foo-bar" :"atom number #{index}"
-
Avoid trailing whitespaces. [link]
-
End each file with a newline. [link]
-
When dealing with lists, maps, structs, or tuples whose elements span over multiple lines and are on separate lines with regard to the enclosing brackets, it's advised to use a trailing comma even for the last element: [link]
[ :foo, :bar, :baz, ]
-
Avoid aligning expression groups: [link]
# Bad module = env.module arity = length(args) def inspect(false), do: "false" def inspect(true), do: "true" def inspect(nil), do: "nil" # Good module = env.module arity = length(args) def inspect(false), do: "false" def inspect(true), do: "true" def inspect(nil), do: "nil"
The same non-alignment rule applies to
<-
and->
clauses as well.
Syntax
-
Always use parentheses around
def
arguments, don't omit them even when a function has no arguments. [link]# Bad def main arg1, arg2 do #... end def main do #... end # Good def main(arg1, arg2) do #... end def main() do #... end
-
Parentheses are a must for local zero-arity function calls and definitions. [link]
# Bad pid = self def new, do: %MapSet{} # Good pid = self() def new(), do: %MapSet{} config = IEx.Config.new
The same applies to local one-arity function calls in pipelines.
String.strip(input) |> decode()
-
Never wrap the arguments of anonymous functions in parentheses. [link]
# Bad Agent.get(pid, fn(state) -> state end) Enum.reduce(numbers, fn(number, acc) -> acc number end) # Good Agent.get(pid, fn state -> state end) Enum.reduce(numbers, fn number, acc -> acc number end)
-
Favor the pipeline operator
|>
to chain function calls together. [link]# Bad String.downcase(String.strip(input)) # Good input |> String.strip |> String.downcase String.strip(input) |> String.downcase
Use a single level of indentation for multi-line pipelines.
String.strip(input) |> String.downcase |> String.slice(1, 3)
-
Avoid needless pipelines like the plague. [link]
# Bad result = input |> String.strip # Good result = String.strip(input)
-
When making a multi-line expression, keep binary operators (the only exception is the
|>
operator) at the ends of the lines. [link]# Bad "No matching message.\n" <> "Process mailbox:\n" <> mailbox # Good "No matching message.\n" <> "Process mailbox:\n" <> mailbox
-
Use the indentation shown below for the
with
special form: [link]with {year, ""} <- Integer.parse(year), {month, ""} <- Integer.parse(month), {day, ""} <- Integer.parse(day) do new(year, month, day) else _ -> {:error, :invalid_format} end
Always use the indentation above if there's an
else
option. If there isn't, the following indentation works as well:with {:ok, date} <- Calendar.ISO.date(year, month, day), {:ok, time} <- Time.new(hour, minute, second, microsecond), do: new(date, time)
-
Use the indentation shown below for the
for
special form: [link]for {alias, _module} <- aliases_from_env(server), [name] = Module.split(alias), starts_with?(name, hint), into: [] do %{kind: :module, type: :alias, name: name} end
If the body of the
do
block is short, the following indentation works as well:for partition <- 0..(partitions - 1), pair <- safe_lookup(registry, partition, key), into: [], do: pair
-
Never use
unless
withelse
. Rewrite these with the positive case first. [link]# Bad unless Enum.empty?(coll) do :ok else :error end # Good if Enum.empty?(coll) do :error else :ok end
-
Omit
else
option inif
andunless
clauses if it returnsnil
. [link]# Bad if byte_size(data) > 0, do: data, else: nil # Good if byte_size(data) > 0, do: data
-
If you have an always-matching clause in the
cond
special form, usetrue
as its condition. [link]# Bad cond do char in ?0..?9 -> char - ?0 char in ?A..?Z -> char - ?A 10 :other -> char - ?a 10 end # Good cond do char in ?0..?9 -> char - ?0 char in ?A..?Z -> char - ?A 10 true -> char - ?a 10 end
-
Never use
||
,&&
and!
for strictly boolean checks. Use these operators only if any of the arguments are non-boolean. [link]# Bad is_atom(name) && name != nil is_binary(task) || is_atom(task) # Good is_atom(name) and name != nil is_binary(task) or is_atom(task) line && line != 0 file || "sample.exs"
-
Favor the binary concatenation operator
<>
over bitstring syntax for patterns matching binaries. [link]# Bad <<"http://", _rest::bytes>> = input <<first::utf8, rest::bytes>> = input # Good "http://" <> _rest = input <<first::utf8>> <> rest = input
-
Use uppercase in definition of hex literals. [link]
# Bad <<0xef, 0xbb, 0xbf>> # Good <<0xEF, 0xBB, 0xBF>>
Naming
-
Use
snake_case
for atoms, functions, variables and module attributes. [link]# Bad :"no match" :Error :badReturn fileName = "sample.txt" @_VERSION "0.0.1" def readFile(path) do #... end # Good :no_match :error :bad_return file_name = "sample.txt" @version "0.0.1" def read_file(path) do #... end
-
Use
CamelCase
for module names. [link]# Bad defmodule :appStack do #... end defmodule App_Stack do #... end defmodule Appstack do #... end # Good defmodule AppStack do #... end
-
The names of predicate functions (a function that return a boolean value) should have a trailing question mark
?
rather than a leadinghas_
or similar. [link]def leap?(year) do #... end
Always use a leading
is_
when naming guard-safe predicate macros.defmacro is_date(month, day) do #... end
-
Use
snake_case
for naming directories and files, e.g.lib/my_app/task_server.ex
. [link] -
Avoid using one-letter variable names. [link]
Comments
Remember, good code is like a good joke: It needs no explanation.
— Russ Olsen
-
Write self-documenting code and ignore the rest of this section. Seriously! [link]
-
Use one space between the leading
#
character of the comment and the text of the comment. [link] -
Avoid superfluous comments. [link]
# Bad String.first(input) # Get first grapheme.
Modules
-
Use a consistent structure when calling
use
/import
/alias
/require
: call them in this order and group multiple calls to each of them. [link]use GenServer import Bitwise import Kernel, except: [length: 1] alias Mix.Utils alias MapSet, as: Set require Logger
-
Use
__MODULE__
pseudo-variable to reference current module. [link]# Bad :ets.new(Kernel.LexicalTracker, [:named_table]) GenServer.start_link(Module.LocalsTracker, nil, []) # Good :ets.new(__MODULE__, [:named_table]) GenServer.start_link(__MODULE__, nil, [])
Regular Expressions
-
Regular expressions are the last resort. Pattern matching and
String
module are things to start with. [link]# Bad Regex.run(~r/#(\d{2})(\d{2})(\d{2})/, color) Regex.match?(~r/(email|password)/, input) # Good <<?#, p1::2-bytes, p2::2-bytes, p3::2-bytes>> = color String.contains?(input, ["email", "password"])
-
Use non-capturing groups when you don't use the captured result. [link]
~r/(?:post|zip )code: (\d )/
-
Be careful with
^
and$
as they match start and end of the line respectively. If you want to match the whole string use:\A
and\z
(not to be confused with\Z
which is the equivalent of\n?\z
). [link]
Structs
-
When calling
defstruct/1
, don't explicitly specifynil
for fields that default tonil
. [link]# Bad defstruct first_name: nil, last_name: nil, admin?: false # Good defstruct [:first_name, :last_name, admin?: false]
Exceptions
-
Make exception names end with a trailing
Error
. [link]# Bad BadResponse ResponseException # Good ResponseError
-
Use non-capitalized error messages when raising exceptions, with no trailing punctuation. [link]
# Bad raise ArgumentError, "Malformed payload." # Good raise ArgumentError, "malformed payload"
There is one exception to the rule - always capitalize Mix error messages.
Mix.raise "Could not find dependency"
ExUnit
-
When asserting (or refuting) something with comparison operators (such as
==
,<
,>=
, and similar), put the expression being tested on the left-hand side of the operator and the value you're testing against on the right-hand side. [link]# Bad assert "héllo" == Atom.to_string(:"héllo") # Good assert Atom.to_string(:"héllo") == "héllo"
When using the match operator
=
, put the pattern on the left-hand side (as it won't work otherwise).assert {:error, _reason} = File.stat("./non_existent_file")
License
This work was created by Aleksei Magusev and is licensed under the CC BY 4.0 license.
Credits
The structure of the guide and some points that are applicable to Elixir were taken from the community-driven Ruby coding style guide.