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
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 the
else
option inif
andunless
constructs ifelse
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 <<"https://", _rest::bytes>> = input <<first::utf8, rest::bytes>> = input # Good "https://" <> _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 leadinghas_
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 examplelib/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 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]
-
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.