Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

https://github.com/lexmag/elixir-style-guide

An opinionated Elixir style guide
https://github.com/lexmag/elixir-style-guide

elixir style-guide

Last synced: about 2 months ago
JSON representation

An opinionated Elixir style guide

Lists

README

        

# 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

* [Linting](#linting)
* [Naming](#naming)
* [Comments](#comments)
* [Modules](#modules)
* [Regular Expressions](#regular-expressions)
* [Structs](#structs)
* [Exceptions](#exceptions)
* [ExUnit](#exunit)

The following section are automatically applied by the code formatter in Elixir v1.6 and listed here only for documentation purposes:

* [Formatting](#formatting)
* [Whitespace](#whitespace)
* [Indentation](#indentation)
* [Term representation](#term-representation)
* [Parentheses](#parentheses)
* [Layout](#layout)

## Linting

*
Favor the pipeline operator `|>` to chain function calls together.
[[link](#pipeline-operator)]

```elixir
# 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.

```elixir
input
|> String.strip()
|> String.downcase()
|> String.slice(1, 3)

*
Avoid needless pipelines like the plague.
[[link](#needless-pipeline)]

```elixir
# Bad
result = input |> String.strip()

# Good
result = String.strip(input)
```

*
Don't use anonymous functions in pipelines.
[[link](#anonymous-pipeline)]

```elixir
# 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:

```elixir
# Good
sentence
|> String.split(~r/\s/)
|> prepend(@sentence_start)
|> Enum.join(" ")
```

*
Never use `unless` with `else`. Rewrite these with the positive case first.
[[link](#no-else-with-unless)]

```elixir
# 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](#no-nil-else)]

```elixir
# 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](#true-in-cond)]

```elixir
# 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](#boolean-operators)]

```elixir
# 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](#patterns-matching-binaries)]

```elixir
# Bad
<<"http://", _rest::bytes>> = input
<> = input

# Good
"http://" <> _rest = input
<> <> rest = input
```

### Naming

*
Use `snake_case` for functions, variables, module attributes, and atoms.
[[link](#snake-case-atoms-funs-vars-attrs)]

```elixir
# 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](#camelcase-modules)]

```elixir
# 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](#predicate-funs-name)]

```elixir
# Bad
def is_leap(year) do
# ...
end

# Good
def leap?(year) do
# ...
end
```

Always use a leading `is_` when naming guard-safe predicate macros.

```elixir
defmacro is_date(month, day) do
# ...
end
```

*
Use `snake_case` for naming directories and files, for example `lib/my_app/task_server.ex`.
[[link](#snake-case-dirs-files)]

*
Avoid using one-letter variable names.
[[link](#one-letter-var)]

### Comments

> 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](#critical-comments)]

*
Avoid superfluous comments.
[[link](#no-superfluous-comments)]

```elixir
# 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](#module-layout)]

```elixir
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](#current-module-reference)]

```elixir
# 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 the `String` module are things to start with.
[[link](#pattern-matching-over-regexp)]

```elixir
# 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](#non-capturing-regexp)]

```elixir
~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](#caret-and-dollar-regexp)]

### Structs

*
When calling `defstruct/1`, don't explicitly specify `nil` for fields that default to `nil`.
[[link](#defstruct-fields-default)]

```elixir
# 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](#exception-naming)]

```elixir
# Bad
BadResponse
ResponseException

# Good
ResponseError
```

*
Use non-capitalized error messages when raising exceptions, with no trailing punctuation.
[[link](#exception-message)]

```elixir
# Bad
raise ArgumentError, "Malformed payload."

# Good
raise ArgumentError, "malformed payload"
```

There is one exception to the rule - always capitalize Mix error messages.

```elixir
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](#exunit-assertion-side)]

```elixir
# 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).

```elixir
assert {:error, _reason} = File.stat("./non_existent_file")
```

## Formatting

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

> 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](#no-trailing-whitespaces)]

*
End each file with a newline.
[[link](#newline-eof)]

*
Use two __spaces__ per indentation level. No hard tabs.
[[link](#spaces-indentation)]

```elixir
# 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](#spaces-in-code)]

```elixir
# 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](#no-spaces-in-code)]

```elixir
# 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](#default-arguments)]

```elixir
# Bad
def start_link(fun, options\\[])

# Good
def start_link(fun, options \\ [])
```

*
Do not put spaces around segment options definition in bitstrings.
[[link](#bitstring-segment-options)]

```elixir
# 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](#leading-space-comment)]

```elixir
# 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](#space-before-anonymous-fun-arrow)]

```elixir
# Bad
Task.async(fn->
ExUnit.Diff.script(left, right)
end)

# Good
Task.async(fn ->
ExUnit.Diff.script(left, right)
end)
```

### Indentation

*

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](#binary-ops-indentation)]

```elixir
# 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-indentation)]

```elixir
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:

```elixir
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-indentation)]

```elixir
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:

```elixir
for partition <- 0..(partitions - 1),
pair <- safe_lookup(registry, partition, key),
into: [],
do: pair
```

*
Avoid aligning expression groups:
[[link](#expression-group-alignment)]

```elixir
# 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](#pipeline-indentation)]

```elixir
input
|> String.strip()
|> String.downcase()
|> String.slice(1, 3)
```

### Term representation

*
Add underscores to decimal literals that have six or more digits.
[[link](#underscores-in-numerics)]

```elixir
# Bad
num = 1000000
num = 1_500

# Good
num = 1_000_000
num = 1500
```

*
Use uppercase letters when using hex literals.
[[link](#hex-literals)]

```elixir
# 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](#quotes-around-atoms)]

```elixir
# 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](#trailing-comma)]

```elixir
[
:foo,
:bar,
:baz
]
```

### Parentheses

*
Parentheses are a must for __local__ or __imported__ zero-arity function calls.
[[link](#zero-arity-parens)]

```elixir
# 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:

```elixir
# Bad
Mix.env

# Good
Mix.env()
```

This rule also applies to one-arity function calls (both local and remote) in pipelines:

```elixir
# Bad
input
|> String.strip
|> decode

# Good
input
|> String.strip()
|> decode()
```

*
Never wrap the arguments of anonymous functions in parentheses.
[[link](#anonymous-fun-parens)]

```elixir
# 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](#fun-parens)]

```elixir
# 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](#parens-in-zero-arity-types)]

```elixir
# Bad
@spec start_link(module, term, Keyword.t) :: on_start

# Good
@spec start_link(module(), term(), Keyword.t()) :: on_start()
```

### Layout

*
Use one expression per line. Don't use semicolons (`;`) to separate statements and expressions.
[[link](#no-semicolon)]

```elixir
# 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](#multi-line-expr-assignment)]

```elixir
# 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](#binary-operators-at-eols)]

```elixir
# 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()
```

## License

This work was created by Aleksei Magusev and is licensed under [the CC BY 4.0 license](https://creativecommons.org/licenses/by/4.0).

![Creative Commons License](http://i.creativecommons.org/l/by/4.0/88x31.png)

## Credits

The structure of the guide and some points that are applicable to Elixir were taken from [the community-driven Ruby coding style guide](https://github.com/bbatsov/ruby-style-guide).