Surgex.Guide.CodeStyle (Surgex v5.0.0) View Source
Basic code style and formatting guidelines.
Link to this section Summary
Functions
Aliases should be preferred over using full module name.
Multi-line calculations should be indented by one level for assignment.
Vertical blocks should be preferred over horizontal blocks.
Indentation blocks must never start or end with blank lines.
Indentation blocks should be padded from surrounding code with single blank line.
There must be no space put before }
, ]
or )
and after {
, [
or (
brackets.
Single space must be put after commas.
Config calls should be placed in alphabetical order, with modules over atoms.
Documentation in @doc
and @moduledoc
should be written in ExDoc-friendly Markdown.
There must be no blank lines between @doc
and the function definition.
Documentation in @doc
and @moduledoc
should start with an one-line summary sentence.
Keywords in Ecto queries should be indented by one level (and one more for on
after join
).
Exception modules (and only them) should be named with the Error
suffix.
Exceptions should define semantic struct fields and a custom message/1
function.
Functions should be called with parentheses.
Indentation must be done with 2 spaces.
Inline blocks should be preferred for simple code that fits one line.
Lines must not be longer than 100 characters.
Hardcoded word (both string and atom) lists should be written using the ~w
sigil.
Macros should be called without parentheses.
Single blank line must be inserted after @moduledoc
.
There must be no space put after the !
operator.
Large numbers must be padded with underscores.
Single space must be put around operators.
Pipe chains must be aligned into multiple lines.
Pipe chains must be started with a plain value.
Pipe chains must be used only for multiple function calls.
RESTful actions should be placed in I S N C E U D
order in controllers and their tests.
Reuse directives against same module should be grouped with {}
syntax and sorted A-Z.
Calls to reuse directives should be placed in use
, require
, import
,alias
order.
Reuse directives should be placed on top of modules or functions.
Per-function usage of reuse directives should be preferred over module-wide usage.
Calls to reuse directives should not be separated with blank lines.
;
must not be used to separate statements and expressions.
Basic happy case in a test file or scope should be placed on top of other cases.
Files must end with single line break.
Lines must not end with trailing white-space.
Modules referenced in typespecs should be aliased.
Link to this section Functions
Aliases should be preferred over using full module name.
Reasoning
Aliasing modules makes code more compact and easier to read. They're even more beneficial as the number of uses of aliased module grows.
That's of course assuming they don't override other used modules or ones that may be used in the
future (such as stdlib's IO
or similar).
Examples
Preferred:
def create(params)
alias Toolbox.Creator
params
|> Creator.build()
|> Creator.call()
|> Toolbox.IO.write()
end
Not so DRY:
def create(params)
params
|> Toolbox.Creator.build()
|> Toolbox.Creator.call()
|> Toolbox.IO.write()
end
Overriding standard library:
def create(params)
alias Toolbox.IO
params
|> Toolbox.Creator.build()
|> Toolbox.Creator.call()
|> IO.write()
end
Multi-line calculations should be indented by one level for assignment.
Reasoning
Horizontal alignment is something especially tempting in Elixir programming as there are many operators and structures that look cool when it gets applied. In particular, pipe chains only look good when the pipe "comes out" from the initial value. In order to achieve that in assignment, vertical alignment is often overused.
The issue is with future-proofness of such alignment. For instance, it may easily get ruined
without developer's attention in typical find-and-replace sessions that touch the name on the left
side of =
sign.
Hence this rule, which is about inserting a new line after the =
and indenting the right side
calculation by one level.
Examples
Preferred:
user =
User
|> build_query()
|> apply_scoping()
|> Repo.one()
Cool yet not so future-proof:
user = User
|> build_query()
|> apply_scoping()
|> Repo.one()
Find-and-replace session result on the above:
authorized_user = User
|> build_query()
|> apply_scoping()
|> Repo.one()
Vertical blocks should be preferred over horizontal blocks.
Reasoning
There's often more than one way to achieve the same and the difference is in fitting things horizontally through indentation vs vertically through function composition. This rule is about preference of the latter over the former in order to avoid crazy indentation, have more smaller functions, which makes for a code easier to understand and extend.
Examples
Too much crazy indentation to fit everything in one function:
defp map_array(array) do
array
|> Enum.uniq
|> Enum.map(fn array_item ->
if is_binary(array_item) do
array_item <> " (changed)"
else
array_item + 1
end
end)
end
Preferred refactor of the above:
defp map_array(array) do
array
|> Enum.uniq
|> Enum.map(&map_array_item/1)
end
defp map_array_item(array_item) when is_binary(array_item), do: array_item <> " (changed)"
defp map_array_item(array_item), do: array_item + 1
Indentation blocks must never start or end with blank lines.
Reasoning
There's no point in adding additional vertical spacing since we already have horizontal padding increase/decrease on block start/end.
Examples
Preferred:
def parent do
nil
end
Wasted line:
def parent do
nil
end
Indentation blocks should be padded from surrounding code with single blank line.
Reasoning
There are probably as many approaches to inserting blank lines between regular code as there are developers, but the common aim usually is to break the heaviest parts into separate "blocks". This rule tries to highlight one most obvious candidate for such "block" which is... an actual block.
Since blocks are indented on the inside, there's no point in padding them there, but the outer
parts of the block (the line where do
appears and the line where end
appears) often include a
key to a reasoning about the whole block and are often the most important parts of the whole
parent scope, so it may be beneficial to make that part distinct.
In case of Elixir it's even more important, since block openings often include non-trivial destructuring, pattern matching, wrapping things in tuples etc.
Examples
Preferred (there's blank line before the Enum.map
block since there's code (array = [1, 2, 3]
)
in parent block, but there's no blank line after that block since there's no more code after it):
def parent do
array = [1, 2, 3]
Enum.map(array, fn number ->
number + 1
end)
end
Obfuscated block:
def parent do
array = [1, 2, 3]
big_numbers = Enum.map(array, fn number ->
number + 1
end)
big_numbers ++ [5, 6, 7]
end
There must be no space put before }
, ]
or )
and after {
, [
or (
brackets.
Reasoning
It's often tempting to add inner padding for tuples, maps, lists or function arguments to give those constructs more space to breathe, but these structures are distinct enough to be readable without it. They may actually be more readable without the padding, because this rule plays well with other spacing rules (like comma spacing or operator spacing), making expressions that combine brackets and operators have a distinct, nicely parse-able "rhythm".
Also, when allowed to pad brackets, developers tend to add such padding inconsistently - even between opening and ending in single line. This gets even worse once a different developer modifies such code and has a different approach towards bracket spacing.
Lastly, it keeps pattern matchings more compact and readable, which invites developers to utilize this wonderful Elixir feature to the fullest.
Examples
Preferred:
def func(%{first: second}, [head | tail]), do: nil
Everything padded and unreadable (no "rhythm"):
def func( %{ first: second }, [ head | tail ] ), do: nil
Inconsistencies:
def func( %{first: second}, [head | tail]), do: nil
Single space must be put after commas.
Reasoning
It's a convention that passes through many languages - it looks good and so there's no reason to make an exception for Elixir on this one.
Examples
Preferred:
fn(arg, %{first: first, second: second}), do: nil
Three creative ways to achieve pure ugliness by omitting comma between arguments, map keys or
before inline do
:
fn(arg,%{first: first,second: second}),do: nil
Config calls should be placed in alphabetical order, with modules over atoms.
Reasoning
Provides obvious and predictable placement of specific config calls.
Examples
Preferred:
config :another_package, key: value
config :my_project, MyProject.A, key: "value"
config :my_project, MyProject.B, key: "value"
config :my_project, :a, key: "value"
config :my_project, :b, key: "value"
config :package, key: "value"
Modules wrongly mixed with atoms and internal props wrongly before external ones:
config :my_project, MyProject.A, key: "value"
config :my_project, :a, key: "value"
config :my_project, MyProject.B, key: "value"
config :my_project, :b, key: "value"
config :another_package, key: value
config :package, key: "value"
Documentation in @doc
and @moduledoc
should be written in ExDoc-friendly Markdown.
Here's what is considered an ExDoc-friendly Markdown:
Paragraphs written with full sentences, separated by a blank line
Headings starting from 2nd level heading (
## Biggest heading
)Bullet lists starting with a dash and subsequent lines indented by 2 spaces
Bullet/ordered list items separated by a blank line
Elixir code indented by 4 spaces to mark the code block
Reasoning
This syntax is encouraged in popular Elixir libraries, it's confirmed to generate nicely readable output and it's just as readable in the code which embeds it as well.
Examples
Preferred:
defmodule MyProject.Accounts do
@moduledoc """
User account authorization and management system.
This module does truly amazing stuff. It's purpose is to take anything you pass its way and
make an user out of that. It can also tell you if specific user can do specific things without
messing the system too much.
Here's what you can expect from this module:
- Nicely written lists with a lot of precious information that
get indented properly in every subsequent line
- And that are well padded as well
And here's an Elixir code example:
defmodule MyProject.Accounts.User do
@defstruct [:name, :email]
end
It's all beautiful, isn't it?
"""
end
Messed up line breaks, messed up list item indentation and non ExDoc-ish code block:
defmodule MyProject.Accounts do
@moduledoc """
User account authorization and management system.
This module does truly amazing stuff. It's purpose is to take anything you pass its way and
make an user out of that. It can also tell you if specific user can do specific things without
messing the system too much.
Here's what you can expect from this module:
- Nicely written lists with a lot of precious information that
get indented properly in every subsequent line
- And that are well padded as well
And here's an Elixir code example:
```
defmodule MyProject.Accounts.User do
@defstruct [:name, :email]
end
```
It's not so beautiful, is it?
"""
end
There must be no blank lines between @doc
and the function definition.
Reasoning
Compared to moduledoc spacing, the @doc
clause belongs to the function
definition directly beneath it, so the lack of blank line between the two is there to make this
linkage obvious. If the blank line is there, there's a growing risk of @doc
clause becoming
completely separated from its owner in the heat of future battles.
Examples
Preferred:
@doc """
This is by far the most complex function in the universe.
"""
def func, do: nil
Weak linkage:
@doc """
This is by far the most complex function in the universe.
"""
def func, do: nil
Broken linkage:
@doc """
This is by far the most complex function in the universe.
"""
def non_complex_func, do: something_less_complex_than_returning_nil()
def func, do: nil
Documentation in @doc
and @moduledoc
should start with an one-line summary sentence.
Reasoning
This first line is treated specially by ExDoc in that it's taken as a module/function summary for API summary listings. The period at its end is removed so that it looks good both as a summary (without the period) and as part of a whole documentation (with a period).
The single-line limit (with up to 100 characters as per line limit rule) is there to avoid mixing up short and very long summaries on a single listing.
It's also important to fit as precise description as possible in this single line, without unnecessarily repeating what's already expressed in the module or function name itself.
Examples
Preferred:
defmodule MyProject.Accounts do
@moduledoc """
User account authorization and management system.
"""
end
Too vague:
defmodule MyProject.Accounts do
@moduledoc """
Accounts system.
"""
end
Missing trailing period:
defmodule MyProject.Accounts do
@moduledoc """
Accounts system
"""
end
Missing trailing blank line:
defmodule MyProject.Accounts do
@moduledoc """
User account authorization and management system.
All functions take the `MyProject.Accounts.Input` structure as input argument.
"""
end
Keywords in Ecto queries should be indented by one level (and one more for on
after join
).
Reasoning
Horizontal alignment is something especially tempting in Elixir programming as there are many
operators and structures that look cool when it gets applied. In particular, Ecto queries are
often written (and they do look good) when aligned to :
after from
macro keywords. In order to
achieve that, vertical alignment is often overused.
The issue is with future-proofness of such alignment. For instance, it'll get ruined when longer
keyword will have to be added, such as preload
or select
in queries with only join
or
where
.
It's totally possible to adhere to the 2 space indentation rule and yet to write a good looking
and readable Ecto query. In order to make things more readable, additional 2 spaces can be added
for contextual indentation of sub-keywords, like on
after join
.
Examples
Preferred:
from users in User,
join: credit_cards in assoc(users, :credit_card),
on: is_nil(credit_cards.deleted_at),
where: is_nil(users.deleted_at),
select: users.id,
preload: [:credit_card],
Cool yet not so future-proof:
from users in User,
join: credit_cards in assoc(users, :credit_card),
on: is_nil(credit_cards.deleted_at),
where: is_nil(users.deleted_at)
Exception modules (and only them) should be named with the Error
suffix.
Reasoning
Exceptions are a distinct kind of application entities, so it's good to emphasize that in their
naming. Two most popular suffixes are Exception
and Error
. The latter was choosen for brevity.
Examples
Preferred:
defmodule InvalidCredentialsError do
defexception [:one, :other]
end
Invalid suffix:
defmodule InvalidCredentialsException do
defexception [:one, :other]
end
Usage of Error
suffix for non-exception modules:
defmodule Actions.HandleRegistrationError do
# ...
end
Exceptions should define semantic struct fields and a custom message/1
function.
Reasoning
It's possible to define an exception with custom arguments and message by overriding the
exception/1
function and defining a standard defexception [:message]
struct, but that yields
to non-semantic exceptions that don't express their arguments in their structure. It also makes
it harder (or at least inconsistent) to define multi-argument exceptions, which is simply a
consequence of not having a struct defined for an actual struct.
Therefore, it's better to define exceptions with a custom set of struct fields instead of a
message
field and to define a message/1
function that takes those fields and creates an error
message out of them.
Examples
Preferred:
defmodule MyError do
defexception [:a, :b]
def message(%__MODULE__{a: a, b: b}) do
"a: #{a}, b: #{b}"
end
end
raise MyError, a: 1, b: 2
Non-semantic error struct with unnamed fields in multi-argument call:
defmodule MyError do
defexception [:message]
def exception({a, b}) do
%__MODULE__{message: "a: #{a}, b: #{b}"}
end
end
raise MyError, {1, 2}
Functions should be called with parentheses.
Reasoning
There's a convention in Elixir universe to make function calls distinct from macro calls by consistently covering them with parentheses. Function calls often take part in multiple operations in a single line or inside pipes and as such, it's just safer to mark the precedence via parentheses.
Examples
Preferred:
first() && second(arg)
Unreadable and with compiler warning coming up:
first && second arg
Indentation must be done with 2 spaces.
Reasoning
This is kind of a delicate subject, but seemingly both Elixir and Ruby communities usually go for spaces, so it's best to stay aligned.
When it comes to linting, the use of specific number of spaces works well with the line length rule, while tabs can be expanded to arbitrary number of soft spaces in editor, possibly ruining all the hard work put into staying in line with the column limit.
As to the number of spaces, 2 seems to be optimal to allow unconstrained module, function and block indentation without sacrificing too many columns.
Examples
Preferred:
defmodule User do
def blocked?(user) do
!user.confirmed || user.blocked
end
end
Too deep indentation (and usual outcome of using tabs):
defmodule User do
def blocked?(user) do
!user.confirmed || user.blocked
end
end
Missing single space:
defmodule User do
def blocked?(user) do
!user.confirmed || user.blocked
end
end
Inline blocks should be preferred for simple code that fits one line.
Reasoning
In case of simple and small functions, conditions etc, the inline variant of block allows to keep code more compact and fit biggest piece of the story on the screen without losing readability.
Examples
Preferred:
def add_two(number), do: number + 2
Wasted vertical space:
def add_two(number) do
number + 2
end
Too long (or too complex) to be inlined:
def add_two_and_multiply_by_the_meaning_of_life_and_more(number),
do: (number + 2) * 42 * get_more_for_this_truly_crazy_computation(number)
Lines must not be longer than 100 characters.
Reasoning
The old-school 70 or 80 column limits seem way limiting for Elixir which is highly based on indenting blocks. Considering modern screen resolutions, 100 columns should work well for anyone with something more modern than CGA video card.
Also, 100 column limit plays well with GitHub, CodeClimate, HexDocs and others.
Examples
Preferred:
defmodule MyProject.Accounts.User do
def build(%{
"first_name" => first_name,
"last_name" => last_name,
"email" => email,
"phone_number" => phone_number
}) do
%__MODULE__{
first_name: first_name,
last_name: last_name,
email: email,
phone_number: phone_number
}
end
end
Missing line breaks before limit:
defmodule MyProject.Accounts.User do
def build(%{"first_name" => first_name, "last_name" => last_name, "email" => email, "phone_number" => phone_number}) do
%__MODULE__{first_name: first_name, last_name: last_name, email: email, phone_number: phone_number}
end
end
Hardcoded word (both string and atom) lists should be written using the ~w
sigil.
Reasoning
They're simply more compact and easier to read this way. They're also easier to extend. For long lists, line breaks can be applied without problems.
Examples
Preferred:
~w(one two three)
~w(one two three)a
Harder to read:
["one", "two", "three"]
[:one, :two, :three]
Macros should be called without parentheses.
Reasoning
There's a convention in Elixir universe to make function calls distinct from macro calls by consistently covering them with parentheses. Compared to functions, macros are often used as a DSL, with one macro invocation per line. As such, they can be safely written (and just look better) without parentheses.
Examples
Preferred:
if bool, do: nil
from t in table, select: t.id
Macro call that looks like a function call:
from(t in table, select: t.id)
Single blank line must be inserted after @moduledoc
.
Reasoning
@moduledoc
is a module-wide introduction to the module. It makes sense to give it padding and
separate it from what's coming next. The reverse looks especially bad when followed by a function
that has no @doc
clause yet.
Examples
Preferred:
defmodule SuperMod do
@moduledoc """
This module is seriously amazing.
"""
def call, do: nil
end
@moduledoc
that pretends to be a @doc
:
defmodule SuperMod do
@moduledoc """
This module is seriously amazing.
"""
def call, do: nil
end
There must be no space put after the !
operator.
Reasoning
Like with brackets, it may be tempting to pad negation to make it more visible, but in general unary operators tend to be easier to parse when they live close to their argument. Why? Because they usually have precedence over binary operators and padding them away from their argument makes this precedence less apparent.
Examples
Preferred:
!blocked && allowed
Operator precedence mixed up:
! blocked && allowed
Large numbers must be padded with underscores.
Reasoning
They're just more readable that way. It's one of those cases when a minimal effort can lead to eternal gratitude from other committers.
Examples
Preferred:
x = 50_000_000
"How many zeros is that" puzzle (hint: not as many as in previous example):
x = 5000000
Single space must be put around operators.
Reasoning
It's a matter of keeping variable names readable and distinct in operator-intensive situations.
There should be no technical problem with such formatting even in long lines, since those can be easily broken into multiple, properly indented lines.
Examples
Preferred:
(a + b) / c
Hard to read:
(a+b)/c
Pipe chains must be aligned into multiple lines.
Check out
Surgex.Guide.CodeStyle.assignment_indentation/0
to see how to assign the output from properly formatted multi-line chains.
Reasoning
This comes from general preference of vertical spacing over horizontal spacing, expressed across
this guide by rules such as Surgex.Guide.CodeStyle.block_alignment/0
. This ensures that the code
is readable and not too condensed. Also, it's easier to modify or extend multi-line chains,
because they don't require re-aligning the whole thing.
By the way, single-line chains look kinda like a code copied from iex
in a hurry, which is only
fine when the building was on fire during the coding session.
Examples
Preferred:
user
|> reset_password()
|> send_password_reset_email()
Too condensed:
user |> reset_password() |> send_password_reset_email()
Pipe chains must be started with a plain value.
Reasoning
The whole point of pipe chain is to push some value through the chain, end to end. In order to do that consistently, it's best to keep away from starting chains with function calls.
This also makes it easier to see if pipe operator should be used at all - since chain with 2 pipes may get reduced to just 1 pipe when inproperly started with function call, it may falsely look like a case when pipe should not be used at all.
Examples
Preferred:
arg
|> func()
|> other_func()
Chain that lost its reason to live:
func(arg)
|> other_func()
Pipe chains must be used only for multiple function calls.
Reasoning
The whole point of pipe chain is that... well, it must be a chain. As such, single function call does not qualify. Reversely, nesting multiple calls instead of piping them seriously limits the readability of the code.
Examples
Preferred for 2 and more function calls:
arg
|> func()
|> other_func()
Preferred for 1 function call:
yet_another_func(a, b)
Not preferred:
other_func(func(arg))
a |> yet_another_func(b)
RESTful actions should be placed in I S N C E U D
order in controllers and their tests.
Reasoning
It's important to establish a consistent order to make it easier to find actions and their tests, considering that both controller and (especially) controller test files tend to be big at times.
This particular order (index
, show
, new
, create
, edit
, update
, delete
) comes from
the long-standing convention established by both Phoenix and, earlier, Ruby on Rails generators,
so it should be familiar, predictable and non-surprising to existing developers.
Examples
Preferred:
defmodule MyProject.Web.UserController do
use MyProject.Web, :controller
def index(_conn, _params), do: raise("Not implemented")
def show(_conn, _params), do: raise("Not implemented")
def new(_conn, _params), do: raise("Not implemented")
def create(_conn, _params), do: raise("Not implemented")
def edit(_conn, _params), do: raise("Not implemented")
def update(_conn, _params), do: raise("Not implemented")
def delete(_conn, _params), do: raise("Not implemented")
end
Different (CRUD-like) order against the convention:
defmodule MyProject.Web.UserController do
use MyProject.Web, :controller
def index(_conn, _params), do: raise("Not implemented")
def new(_conn, _params), do: raise("Not implemented")
def create(_conn, _params), do: raise("Not implemented")
def show(_conn, _params), do: raise("Not implemented")
def edit(_conn, _params), do: raise("Not implemented")
def update(_conn, _params), do: raise("Not implemented")
def delete(_conn, _params), do: raise("Not implemented")
end
The issue with CRUD order is that
index
action falls between fitting and being kind of "above" the Read section andnew
/edit
actions fall between Read and Create/Update sections, respectively.
Reuse directives against same module should be grouped with {}
syntax and sorted A-Z.
Reasoning
The fresh new grouping feature for alias
, import
, require
and use
allows to make multiple
reuses from single module shorter, more declarative and easier to comprehend. It's just a
challenge to use this feature consistently, hence this rule.
Keeping sub-module names in separate lines (even when they could fit a single line) is an additional investment for the future - to have clean diffs when more modules will get added. It's also easier to keep them in alphabetical order when they're in separate lines from day one.
Examples
Preferred:
alias Toolbox.{
Creator,
Deletor,
Other,
}
alias SomeOther.Mod
Short but not so future-proof:
alias Toolbox.{Creator, Deletor, Other}
Classical but inconsistent and not so future-proof:
alias Toolbox.Creator
alias Toolbox.Deletor
alias SomeOther.Mod
alias Toolbox.Other
Calls to reuse directives should be placed in use
, require
, import
,alias
order.
Reasoning
First of all, having any directive ordering convention definitely beats not having one, since they are a key to parsing code and so it adds up to better code reading experience when you know exactly where to look for an alias or import.
This specific order is an attempt to introduce more significant directives before more trivial
ones. It so happens that in case of reuse directives, the reverse alphabetical order does exactly
that, starting with use
(which can do virtually anything with a target module) and ending with
alias
(which is only a cosmetic change and doesn't affect the module's behavior).
Examples
Preferred:
use Helpers.Thing import Helpers.Other alias Helpers.Tool
Out of order:
alias Helpers.Tool
import Helpers.Other
use Helpers.Thing
Reuse directives should be placed on top of modules or functions.
Reasoning
Calls to alias
, import
, require
or use
should be placed on top of module or function, or
directly below @moduledoc
in case of modules with documentation.
Just like with the order rule, this is to make finding these directives faster when reading the code. For that reason, it's more beneficial to have such important key for interpreting code in obvious place than attempting to have them right above the point where they're needed (which usually ends up messed up anyway when code gets changed over time).
Examples
Preferred:
defmodule Users do
alias Users.User
def name(user) do
user["name"] || user.name
end
def delete(user_id) do
import Ecto.Query
user_id = String.to_integer(user_id)
Repo.delete_all(from users in User, where: users.id == ^user_id)
end
end
Cool yet not so future-proof "lazy" placement:
defmodule Users do
def name(user) do
user["name"] || user.name
end
alias Users.User
def delete(user_id) do
user_id = String.to_integer(user_id)
import Ecto.Query
Repo.delete_all(from users in User, where: users.id == ^user_id)
end
end
Per-function usage of reuse directives should be preferred over module-wide usage.
Reasoning
If a need for alias
, import
or require
spans only across single function in a module (or
across a small subset of functions in otherwise large module), it should be preferred to declare
it locally on top of that function instead of globally for whole module.
Keeping these declarations local makes them even more descriptive as to what scope is really affected. They're also more visible, being closer to the place they're used at. The chance for conflicts is also reduced when they're local.
Examples
Preferred (alias
on Users.User
is used in both create
and delete
functions so it's made
global, but import
on Ecto.Query
is only used in delete
function so it's declared only
there):
defmodule Users do
alias Users.User
def create(params)
%User{}
|> User.changeset(params)
|> Repo.insert()
end
def delete(user_id) do
import Ecto.Query
Repo.delete_all(from users in User, where: users.id == ^user_id)
end
end
Not so DRY (still, this could be OK if there would be more functions in Users
module that
wouldn't use the User
sub-module):
defmodule Users do
def create(params)
alias Users.User
%User{}
|> User.changeset(params)
|> Repo.insert()
end
def delete(user_id) do
import Ecto.Query
alias Users.User
Repo.delete_all(from users in User, where: users.id == ^user_id)
end
end
Everything a bit too public:
defmodule Users do
import Ecto.Query
alias Users.User
def create(params)
%User{}
|> User.changeset(params)
|> Repo.insert()
end
def delete(user_id) do
Repo.delete_all(from users in User, where: users.id == ^user_id)
end
end
Calls to reuse directives should not be separated with blank lines.
Reasoning
It may be tempting to separate all aliases from imports with blank line or to separate multi-line grouped aliases from other aliases, but as long as they're properly placed and ordered, they're readable enough without such extra efforts. Also, as their number grows, it's more beneficial to keep them vertically compact than needlessly padded.
Examples
Preferred:
use Helpers.Thing
import Helpers.Other
alias Helpers.Subhelpers.{
First,
Second
}
alias Helpers.Tool
Too much padding (with actual code starting N screens below):
use Helpers.Thing
import Helpers.Other
alias Helpers.Subhelpers.{
First,
Second
}
alias Helpers.Tool
;
must not be used to separate statements and expressions.
Reasoning
This is the most classical case when it comes to preference of vertical over horizontal alignment.
Let's just keep ;
operator for iex
sessions and focus on code readability over doing code
minification manually - neither EVM nor GitHub will explode over that additional line break.
Actually, ", " costs one more byte than an Unix line break but if that would be our biggest concern then I suppose we wouldn't prefer spaces over tabs for indentation...
Examples
Preferred:
func()
other_func()
iex
session saved to file by mistake:
func(); other_func()
Basic happy case in a test file or scope should be placed on top of other cases.
Reasoning
When using tests to understand how specific unit of code works, it's very handy to have the basic happy case placed on top of other cases.
Examples
Preferred:
defmodule MyProject.Web.MyControllerTest do
describe "index/2" do
test "works for valid params" do
# ...
end
test "fails for invalid params" do
# ...
end
end
end
Out of order:
defmodule MyProject.Web.MyControllerTest do
describe "index/2" do
test "fails for invalid params" do
# ...
end
test "works for valid params" do
# ...
end
end
end
Files must end with single line break.
Reasoning
Many editors and version control systems consider files without final line break invalid. In git, such last line gets highlighted with an alarming red. Like with trailing white-space, it's a bad habit to leave such artifacts and ruin diffs for developers who save files correctly.
Reversely, leaving too many line breaks is just sloppy.
Most editors can be tuned to automatically add single trailing line break on save.
Examples
Preferred:
func()⮐
Missing line break:
func()
Too many line breaks:
func()⮐
⮐
Lines must not end with trailing white-space.
Reasoning
Leaving white-space at the end of lines is a bad programming habit that leads to crazy diffs in version control once developers that do it get mixed with those that don't.
Most editors can be tuned to automatically trim trailing white-space on save.
Examples
Preferred:
func()
Hidden white-space (simulated by adding comment at the end of line):
func() # line end
Modules referenced in typespecs should be aliased.
Reasoning
When writing typespecs, it is often necessary to reference a module in some nested naming
scheme. One could reference it with the absolute name, e.g. Application.Accounting.Invoice.t
,
but this makes typespecs rather lengthy.
Using aliased modules makes typespecs easier to read and, as an added benefit, it allows for an in-front declaration of module dependencies. This way we can easily spot breaches in module isolation.
Examples
Preferred:
alias VideoApp.Recommendations.{Rating, Recommendation, User}
@spec calculate_recommendations(User.t, [Rating.t]) :: [Recommendation.t]
def calculate_recommendations(user, ratings) do
# ...
end
Way too long:
@spec calculate_recommendations(
VideoApp.Recommendations.User.t,
[VideoApp.Recommendations.Rating.t]
) :: [VideoApp.Recommendations.Recommendation.t]
def calculate_recommendations(user, ratings) do
# ...
end