An opinionated 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
The following section are automatically applied by the code formatter in Elixir v1.6 and listed here only for documentation purposes:
Favor the pipeline operator |>
to chain function calls together.
[link]
# Bad
String.downcase(String.strip(input))
# Good
input |> String.strip() |> String.downcase()
For a multi-line pipeline, place each function call on a new line, and retain the level of indentation.
input
|> String.strip()
|> String.downcase()
|> String.slice(1, 3)
Avoid needless pipelines like the plague. [link]
# Bad
result = input |> String.strip()
# Good
result = String.strip(input)
Don't use anonymous functions in pipelines. [link]
# Bad
sentence
|> String.split(~r/\s/)
|> (fn words -> [@sentence_start | words] end).()
|> Enum.join(" ")
# Good
split_sentence = String.split(sentence, ~r/\s/)
Enum.join([@sentence_start | split_sentence], " ")
Consider defining private helper function when appropriate:
# Good
sentence
|> String.split(~r/\s/)
|> prepend(@sentence_start)
|> Enum.join(" ")
Never use unless
with else
. 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 the else
option in if
and unless
constructs if else
returns nil
.
[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, use true
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 snake_case
for functions, variables, module attributes, and atoms.
[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. Keep uppercase acronyms as uppercase.
[link]
# Bad
defmodule :appStack do
# ...
end
defmodule App_Stack do
# ...
end
defmodule Appstack do
# ...
end
defmodule Html do
# ...
end
# Good
defmodule AppStack do
# ...
end
defmodule HTML do
# ...
end
The names of predicate functions (functions that return a boolean value) should have a trailing question mark ?
rather than a leading has_
or similar.
[link]
# Bad
def is_leap(year) do
# ...
end
# Good
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, for example lib/my_app/task_server.ex
.
[link]
Avoid using one-letter variable names. [link]
Remember, good code is like a good joke: It needs no explanation.
— Russ Olsen
Use code comments only to communicate important details to another person reading the code. For example, a high-level description of the algorithm being implemented or why certain critical decisions, such as optimization or business rules, were made. [link]
Avoid superfluous comments. [link]
# Bad
String.first(input) # Get first grapheme.
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 the __MODULE__
pseudo-variable to reference the 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 are the last resort. Pattern matching and the 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]
When calling defstruct/1
, don't explicitly specify nil
for fields that default to nil
.
[link]
# Bad
defstruct first_name: nil, last_name: nil, admin?: false
# Good
defstruct [:first_name, :last_name, admin?: false]
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")
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")
The rules below are automatically applied by the code formatter in Elixir v1.6. They are provided here for documentation purposes and for those maintaining older codebases.
Whitespace might be (mostly) irrelevant to the Elixir compiler, but its proper use is the key to writing easily readable code.
Avoid trailing whitespaces. [link]
End each file with a newline. [link]
Use two spaces per indentation level. No hard tabs. [link]
# Bad
def register_attribute(name, opts) do
register_attribute(__MODULE__, name, opts)
end
# Good
def register_attribute(name, opts) do
register_attribute(__MODULE__, name, opts)
end
Use a space before and after binary operators. Use a space after commas ,
, colons :
, and semicolons ;
. Do not put spaces around matched pairs like brackets []
, braces {}
, and so on.
[link]
# Bad
sum = 1+1
[first|rest] = 'three'
{a1,a2} = {2 ,3}
Enum.join( [ "one" , << "two" >>, sum ])
# Good
sum = 1 + 2
[first | rest] = 'three'
{a1, a2} = {2, 3}
Enum.join(["one", <<"two">>, sum])
Use no spaces after unary operators and inside range literals. The only exception is the not
operator: use a space after it.
[link]
# Bad
angle = - 45
^ result = Float.parse("42.01")
# Good
angle = -45
^result = Float.parse("42.01")
2 in 1..5
not File.exists?(path)
Use spaces around default arguments \\
definition.
[link]
# Bad
def start_link(fun, options\\[])
# Good
def start_link(fun, options \\ [])
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>>
Use one space between the leading #
character of the comment and the text of the comment.
[link]
# Bad
#Amount to take is greater than the number of elements
# Good
# Amount to take is greater than the number of elements
Always use a space before ->
in 0-arity anonymous functions.
[link]
# Bad
Task.async(fn->
ExUnit.Diff.script(left, right)
end)
# Good
Task.async(fn ->
ExUnit.Diff.script(left, right)
end)
Indent the right-hand side of a binary operator one level more than the left-hand side if left-hand side and right-hand side are on different lines. The only exceptions are when
in guards and |>
, which go on the beginning of the line and should be indented at the same level as their left-hand side. Do this also for binary operators when assigning.
[link]
# Bad
"No matching message.\n" <>
"Process mailbox:\n" <>
mailbox
message =
"No matching message.\n" <>
"Process mailbox:\n" <>
mailbox
input
|> String.strip()
|> String.downcase()
defp valid_identifier_char?(char)
when char in ?a..?z
when char in ?A..?Z
when char in ?0..?9
when char == ?_ do
true
end
defp parenless_capture?({op, _meta, _args})
when is_atom(op) and
atom not in @unary_ops and
atom not in @binary_ops do
true
end
# Good
"No matching message.\n" <>
"Process mailbox:\n" <>
mailbox
message =
"No matching message.\n" <>
"Process mailbox:\n" <>
mailbox
input
|> String.strip()
|> String.downcase()
defp valid_identifier_char?(char)
when char in ?a..?z
when char in ?A..?Z
when char in ?0..?9
when char == ?_ do
true
end
defp parenless_capture?({op, _meta, _args})
when is_atom(op) and
atom not in @unary_ops and
atom not in @binary_ops do
true
end
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
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.
Use a single level of indentation for multi-line pipelines. [link]
input
|> String.strip()
|> String.downcase()
|> String.slice(1, 3)
Add underscores to decimal literals that have six or more digits. [link]
# Bad
num = 1000000
num = 1_500
# Good
num = 1_000_000
num = 1500
Use uppercase letters when using hex literals. [link]
# Bad
<<0xef, 0xbb, 0xbf>>
# Good
<<0xEF, 0xBB, 0xBF>>
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}"
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 not use a trailing comma on the last element: [link]
[
:foo,
:bar,
:baz
]
Parentheses are a must for local or imported zero-arity function calls. [link]
# Bad
pid = self
import System, only: [schedulers_online: 0]
schedulers_online
# Good
pid = self()
import System, only: [schedulers_online: 0]
schedulers_online()
The same should be done for remote zero-arity function calls:
# Bad
Mix.env
# Good
Mix.env()
This rule also applies to one-arity function calls (both local and remote) in pipelines:
# Bad
input
|> String.strip
|> decode
# Good
input
|> String.strip()
|> 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)
Always use parentheses around arguments to definitions (such as def
, defp
, defmacro
, defmacrop
, defdelegate
). Don't omit them even when a function has no arguments.
[link]
# Bad
def main arg1, arg2 do
# ...
end
defmacro env do
# ...
end
# Good
def main(arg1, arg2) do
# ...
end
defmacro env() do
# ...
end
Always use parens on zero-arity types. [link]
# Bad
@spec start_link(module, term, Keyword.t) :: on_start
# Good
@spec start_link(module(), term(), Keyword.t()) :: on_start()
Use one expression per line. Don't use semicolons (;
) to separate statements and expressions.
[link]
# Bad
stacktrace = System.stacktrace(); fun.(stacktrace)
# Good
stacktrace = System.stacktrace()
fun.(stacktrace)
When assigning the result of a multi-line expression, begin the expression on a new line. [link]
# Bad
{found, not_found} = files
|> Enum.map(&Path.expand(&1, path))
|> Enum.partition(&File.exists?/1)
prefix = case base do
:binary -> "0b"
:octal -> "0o"
:hex -> "0x"
end
# Good
{found, not_found} =
files
|> Enum.map(&Path.expand(&1, path))
|> Enum.partition(&File.exists?/1)
prefix =
case base do
:binary -> "0b"
:octal -> "0o"
:hex -> "0x"
end
When writing a multi-line expression, keep binary operators at the end of each line. The only exception is the |>
operator (which goes at the beginning of the line).
[link]
# Bad
"No matching message.\n"
<> "Process mailbox:\n"
<> mailbox
input |>
String.strip() |>
decode()
# Good
"No matching message.\n" <>
"Process mailbox:\n" <>
mailbox
input
|> String.strip()
|> decode()
This work was created by Aleksei Magusev and is licensed under the CC BY 4.0 license.
The structure of the guide and some points that are applicable to Elixir were taken from the community-driven Ruby coding style guide.