Elixir 🧪
Linting
The team mainly uses and respects the standardized format from mix format
. The following guidelines are basic highlights from these rules.
Formatting
-
-
:line_length
option in the.formatter.exs
file). -
-
defmodule
-
Naming
-
Use
snake_case
for variables (including atoms and constants) and methods.defp calculateBalance(%Entry{bookType: "debit", amount: amount}, balance) do balance - amount end
defp calculate_balance(%Entry{book_type: "debit", amount: amount}, balance) do balance - amount end
-
Use
PascalCase
for modules.defmodule Payment.jobs.Inquiry_scheduler do end
defmodule Payment.Jobs.InquiryScheduler do end
-
Use
snake_case
for naming files.inquiryScheduler.ex
inquiry_scheduler.ex
-
Use
snake_case
for naming directories.lib/PaymentService/Jobs/inquiry_scheduler.ex
lib/payment_service/jobs/inquiry_scheduler.ex
-
Name methods which could raise an exception with a bang
!
.def create!(registration_uid, customer_params) do attrs = customer_params |> build_attrs!() |> Map.put(:registration_uid, registration_uid) # ... end defp build_attrs!(%{ "alien_id" => alien_id, "citizen_id" => citizen_id, }) do %{ citizen_id: citizen_id, alien_id: alien_id } end defp build_attrs!(params) do raise ArgumentError, message: "Unexpected customer profile params" end
-
Aim to have just one module per source file including structs definition (i.e. create one file for the module and another file for the struct). Name the file name as the module, but replacing CamelCase with snake_case.
# lib/payment/worker.ex defmodule Payment.Worker do defmodule WorkerConfiguration do defstruct [:retry_period_ms, :error_retry_limit] end # ... end
# lib/payment/worker.ex defmodule Payment.Worker do # ... end # lib/payment/worker_configuration.ex defmodule Payment.WorkerConfiguration do defstruct [:retry_period_ms, :error_retry_limit] end
- Prefer using
_variable_name
pattern for naming the ignored variables.
def index(conn, _), do: Plug.Conn.resp(conn, :ok, "success")
def index(conn, _params), do: Plug.Conn.resp(conn, :ok, "success")
- Prefer using
Syntax
-
Use the shorthand syntax to group aliases from the same sub-module.
alias Payment.Ledger.Balance alias Payment.Ledger.EntriesGenerator alias Payment.Ledger.Entry alias Payment.Ledger.Transaction
alias Payment.Ledger.{Balance, EntriesGenerator, Entry, Transaction}
-
Prefer placing the error case at the bottom for better happy-path readability. Unless placing it at the top is necessitated by the casing logic.
|> case do {:error, reason} -> {:error, reason} {:ok, %{status_code: 200}} -> {:ok, "success"} {:ok, %{status_code: 400, body: body}} -> handle_error(body) end
|> case do {:ok, %{status_code: 200}} -> {:ok, "success"} {:ok, %{status_code: 400, body: body}} -> handle_error(body) {:error, reason} -> {:error, reason} end
-
Avoid using the pipe operator just once.
some_string |> String.downcase() System.version() |> Version.parse()
String.downcase(some_string) Version.parse(System.version())
-
Use raw value in the first part of a function chain.
String.trim(some_string) |> String.downcase() |> String.codepoints()
some_string |> String.trim() |> String.downcase() |> String.codepoints()
-
When invoking anonymous functions inside a pipeline, prefer to place them inside
Kernel.then/2
.value |> (&(&1 * 10)).() |> (&(&1 > 200)).()
value |> then(&(&1 * 10)) |> then(&(&1 > 200))
-
Use
do:
for single lineif/unless
statementsif some_condition do "some_stuff" end
if some_condition, do: "some_stuff"
-
Never use
unless
withelse
. Rewrite these with the positive case first.unless success do IO.puts("failure") else IO.puts("success") end
if success do IO.puts("success") else IO.puts("failure") end
Functions
-
Prefer using the single line function as long as
mix format
command satisfies to keep the function body in one line.def validate_coupon() do :ok end def list_by_ids(courier_company_ids) do where(CourierCompany, [company], company.id in ^courier_company_ids) end def build_error_message(purchase, _attrs) when purchase.product.is_shippable == false do "Purchase's product is not shippable" end def create_voucher(attrs \\ %{}), do: %Voucher{} |> change_voucher(attrs) |> Repo.insert()
def validate_coupon(), do: :ok def list_by_ids(courier_company_ids), do: where(CourierCompany, [company], company.id in ^courier_company_ids) def build_error_message(purchase, _attrs) when purchase.product.is_shippable == false, do: "Purchase's product is not shippable" def create_voucher(attrs \\ %{}) do %Voucher{} |> change_voucher(attrs) |> Repo.insert() end
-
If the function
head
anddo: clause
are too long to fit on the same line, then putdo:
on a new line which should be indented by one level.def some_function([:foo, :bar, :baz] = args), do: Enum.map(args, fn arg -> arg <> " is on a very long line!" end)
def some_function([:foo, :bar, :baz] = args), do: Enum.map(args, fn arg -> arg <> " is on a very long line!" end)
-
Do not separate multiple declarations of functions of the same name with new lines.
def Payment do def status(%{failed_at: failed_at}) when not is_nil(failed_at), do: "failed" def status(%{processed_at: nil}), do: "processing" def status(_), do: "processed" end
def Payment do def status(%{failed_at: failed_at}) when not is_nil(failed_at), do: "failed" def status(%{processed_at: nil}), do: "processing" def status(_), do: "processed" end
-
When the
do: clause
starts on its own line, treat it as a multiline function by separating it with a blank line.def some_function(_), do: :very_long_line_here def some_function([]), do: :ok def some_function(nil), do: {:error, "No Value"}
def some_function(_), do: :very_long_line_here def some_function([]), do: :ok def some_function(nil), do: {:error, "No Value"}
-
Prefer pattern matching and guards in function definitions over conditionals in the function body.
defp handle_response({:ok, %Response{status: status, body: body}}) do if status in 200..299 do {:ok, body} else {:error, :internal_server_error, reason} end end
defp handle_response({:ok, %Response{status: status, body: body}}) when status in 200..299, do: {:ok, body} defp handle_response({:error, reason}), do: {:error, :internal_server_error, reason}
-
Prefer to prefix the function name with
maybe_
for functions that may perform in some condition.def validate_user_params(params) do params |> cast_to_changeset() |> maybe_validate_password() end # ... defp maybe_validate_password(%{password: password} = _changeset) do # Do a password validation when the password is present in changeset end # This function clause does nothing when the password is not given defp maybe_validate_password(changeset), do: changeset
-
Prefer placing all private functions at the bottom of the module.
defmodule App.Account.Users do def authenticate(email, password) do # Get user by email and verify password with verify_password/2 end defp verify_password(user, password) do # ... end def full_name(user) do: "#{user.first_name} #{user.last_name}" end
defmodule App.Account.Users do def authenticate(email, password) do # Get user by email and verify password with verify_password/2 end def full_name(user) do: "#{user.first_name} #{user.last_name}" defp verify_password(user, password) do # ... end end
-
Prefer placing
defdelegate
functions at the top of the module, just before the normal functions.defmodule App.UserView do alias App.SharedView # All the other module attributes, directives, and macros go here ... # All the normal functions go here ... defdelegate formatted_username(user), to: SharedView end
defmodule App.UserView do alias App.SharedView # All the other module attributes, directives, and macros go here ... defdelegate formatted_username(user), to: SharedView # All the normal functions go here ... end
Modules
List module attributes, directives, and macros in the following order:
-
@moduledoc
-
@behaviour
-
use
-
import
-
alias
-
require
-
@module_attribute
-
defstruct
-
@callback
and put an empty line between each type.
defmodule Payment.Merchants.Merchant do
@moduledoc """
About the module
"""
@behaviour Parser
use Ecto.Schema
import Ecto.Changeset
alias Payment.ReferenceGenerator
require Foo
@module_attribute :foo
@other_attribute 100
defstruct name: nil, other_attribute: @other_attribute
@callback some_function(term) :: :ok | {:error, term}
end
defmodule Payment.Merchants.Merchant do
@moduledoc """
About the module
"""
@behaviour Parser
use Ecto.Schema
import Ecto.Changeset
alias Payment.ReferenceGenerator
require Foo
@module_attribute :foo
@other_attribute 100
defstruct name: nil, other_attribute: @other_attribute
@callback some_function(term) :: :ok | {:error, term}
end
-
Order the dependencies of a module in alphabetical order
alias PaymentService.Verifications alias PaymentService.Currency alias PaymentService.TopUps
alias PaymentService.{Currency, TopUps, Verifications}
-
Use the
__MODULE__
pseudo-variable when a module refers to itself. This avoids updating any self-references when the module name changes.defmodule SomeProject.SomeModule do defstruct [:name] def name(%SomeProject.SomeModule{name: name}), do: name end defmodule SomeProject.SomeModule do alias __MODULE__, as: SomeModule defstruct [:name] def name(%SomeModule{name: name}), do: name end
defmodule SomeProject.SomeModule do defstruct [:name] def name(%__MODULE__{name: name}), do: name end
-
Avoid repeating fragments in module names and namespaces.
defmodule Todo.Todo do ... end
defmodule Todo.Item do ... end
Structs
-
Use a list of atoms for struct fields that default to nil, followed by the other keywords.
defstruct name: nil, params: nil, active: true
defstruct [:name, :params, active: true]
Macros
Strings
-
Prefer using IO list to build up the output then write it to a file, a socket, or return it from a plug.
def output do foo = "foo" bar = "bar" foo <> bar <> foo <> bar <> foo <> bar end
def output do foo = "foo" bar = "bar" [foo, bar, foo, bar, foo, bar] end
Documentation
Module
-
Use
@moduledoc
to document modules.defmodule Users.PasswordAuthenticator do @moduledoc """ Authenticate a user with email and password """ # ... end
-
Use
@moduledoc false
when a module does not need to be documented.defmodule SomeModule do @moduledoc false # ... end
-
Separate code after the
@moduledoc
with a blank line.defmodule SomeModule do @moduledoc """ About the module """ use AnotherModule end
defmodule SomeModule do @moduledoc """ About the module """ use AnotherModule end
-
Always reference modules by their full name.
defmodule AwesomeModule do @moduledoc """ This module contains `ModuleA` """ # ... end
defmodule AwesomeModule do @moduledoc """ This module contains `AwesomeModule.ModuleA` """ # ... end
Function
-
Use
@doc
to document functions.@doc """ About this function. """ def awesome_function do # ... end
-
Always includes arity when referencing functions.
@doc """ This function is similar to `other_function`. """ def awesome_function do # ... end
@doc """ This function is similar to `other_function/2`. """ def awesome_function do # ... end
-
Module name can be omitted when referencing a local function. However, for function in an external module, the full module name must be included.
defmodule SomeModule do @doc """ This function is similar to `other_function/2` and compatible with `OtherModule.awesome_function/1`. """ def some_function do # ... end @doc """ About this function. """ def other_function(_arg1, _arg2) do # ... end end defmodule OtherModule do @doc """ About this function. """ def awesome_function(_arg) do # ... end end
-
Prefer to include examples with sample outputs under
## Examples
heading to give readers clarification.@doc """ Returns sum of given params or returns an error if given invalid params. ## Examples A typical example when calling a function iex> AwesomeModule.sum(2, 4) 6 iex> AwesomeModule.sum(nil, 1) :error """ @spec sum(integer(), integer()) :: integer() | :error def sum(num1, num2) do # ... end
-
To ensure the document and examples do not get out-of-date, always add Doctest to verify the example’s output within
iex>
prompt.defmodule SomeModule do @doc """ ## Examples iex> SomeModule.add(2, 4) 6 """ def add(arg1, arg2) do arg1 + arg2 end end defmodule SomeModuleTest do use ExUnit.Case, async: true doctest SomeModule end
-
Use built-in typespecs notations for declaring types and specifications for methods. Even if the documentation won’t be exported using a tool like ExDocs, typespecs notations help documenting the usage of methods and code analysis tools like Dialyzer.
@spec fetch_user_detail(String.t()) :: {:ok, %User{}} | {:error, atom(), String.t()} def fetch_user_detail(email) do # ... end
Markdown
-
Start new sections with second level Markdown headers
##
. First level headers are reserved for module and function names.@doc """ Summary of awesome function. ## Options List of supported options. """ def awesome_function(_opts) do # ... end
-
Use Markdown backtick
`
to quote the module or function name, Elixir can generate links to the referenced module/function for the generated documentation.defmodule AwesomeModule do @moduledoc """ This module contains `AwesomeModule.ModuleA` """ # ... end
Testing
To test Elixir applications, use ExUnit.