Elixir 🧪
Linting
We mainly use and respect the standardized format from mix format
. The following guidelines are basic highlights from these rules.
Formatting
- Use soft-tabs with a two-space indent.
- Limit each line of code to fewer than 100 characters (prefer to set the
:line_length
option in the.formatter.exs
file). - Use a single empty line to break between statements to organize logical chunks of code.
- Don’t put a blank line after
defmodule
- End each file with a newline.
Naming
-
Use
snake_case
for variables (including atoms and constants) and methods.# Bad defp calculateBalance(%Entry{bookType: "debit", amount: amount, currency: "THB"}, balance) do balance - amount end # Good defp calculate_balance(%Entry{book_type: "debit", amount: amount, currency: "THB"}, balance) do balance - amount end
-
Use
PascalCase
for modules.# Bad defmodule Payment.jobs.Inquiry_scheduler do end # Good defmodule Payment.Jobs.InquiryScheduler do end
-
Use
snake_case
for naming files.# Bad inquiryScheduler.ex # Good inquiry_scheduler.ex
-
Use
snake_case
for naming directories.# Bad lib/PaymentService/Jobs/inquiry_scheduler.ex # Good 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.
# Bad # lib/payment/worker.ex defmodule Payment.Worker do defmodule WorkerConfiguration do defstruct [:retry_period_ms, :error_retry_limit] end #... end # Good # 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.
# Bad def index(conn, _), do: Plug.Conn.resp(conn, :ok, "success") # Good 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.
# Bad alias Payment.Ledger.Balance alias Payment.Ledger.EntriesGenerator alias Payment.Ledger.Entry alias Payment.Ledger.Transaction # Good 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 tops is necessitated by the casing logic.
# Less Preferred |> case do {:error, reason} -> {:error, reason} {:ok, %{status_code: 200}} -> {:ok, "success"} {:ok, %{status_code: 400, body: body}} -> handle_error(body) end # Better |> 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.
# Less Preferred some_string |> String.downcase() System.version() |> Version.parse() # Preferred String.downcase(some_string) Version.parse(System.version())
-
Use raw value in the first part of a function chain.
# Less Preferred String.trim(some_string) |> String.downcase() |> String.codepoints() # Preferred some_string |> String.trim() |> String.downcase() |> String.codepoints()
-
Use
do:
for single lineif/unless
statements# Less Preferred if some_condition do "some_stuff" end # Preferred if some_condition, do: "some_stuff"
-
Never use unless with else. Rewrite these with the positive case first.
# Bad unless success do IO.puts("failure") else IO.puts("success") end # Good 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 just in one line.# Less Preferred def validate_coupon() do :ok end def list_by_ids(courier_company_ids) do where(CourierCompany, [courier_company], courier_company.id in ^courier_company_ids) end def build_error_message(purchase, _attrs) when purchase.product.is_shippable == false and purchase.product.attachment_type == :digital do "Purchase's product is not shippable" end def create_voucher(attrs \\ %{}), do: %Voucher{} |> change_voucher(attrs) |> Repo.insert() # Preferred def validate_coupon(), do: :ok def list_by_ids(courier_company_ids), do: where(CourierCompany, [courier_company], courier_company.id in ^courier_company_ids) def build_error_message(purchase, _attrs) when purchase.product.is_shippable == false and purchase.product.attachment_type == :digital, 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.# Bad def some_function([:foo, :bar, :baz] = args), do: Enum.map(args, fn arg -> arg <> " is on a very long line!" end) # Good 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 newlines.
# Bad def Payment do def status(%__MODULE__{failed_at: failed_at}) when not is_nil(failed_at), do: "failed" def status(%__MODULE__{processed_at: nil}), do: "processing" def status(_), do: "processed" end # Good def Payment do def status(%__MODULE__{failed_at: failed_at}) when not is_nil(failed_at), do: "failed" def status(%__MODULE__{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 blank lines.# Less Preferred def some_function(_), do: :very_long_line_here def some_function([]), do: :ok def some_function(nil), do: {:error, "No Value"} # Preferred 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.
# Bad 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 # Good 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.
# Bad 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 # Good 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.# Bad 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 # Good 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.
# Bad
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
# Good
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
# Bad alias PaymentService.Verifications alias PaymentService.Currency alias PaymentService.TopUps # Good 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.# Bad defmodule SomeProject.SomeModule do defstruct [:name] def name(%SomeProject.SomeModule{name: name}), do: name end # Less Preferred defmodule SomeProject.SomeModule do alias __MODULE__, as: SomeModule defstruct [:name] def name(%SomeModule{name: name}), do: name end # Preferred defmodule SomeProject.SomeModule do defstruct [:name] def name(%__MODULE__{name: name}), do: name end
-
Avoid repeating fragments in module names and namespaces.
# Less Preferred defmodule Todo.Todo do ... end # Preferred defmodule Todo.Item do ... end
Structs
-
Use a list of atoms for struct fields that default to nil, followed by the other keywords.
# Less Preferred defstruct name: nil, params: nil, active: true # Preferred defstruct [:name, :params, active: true]
Strings
-
Prefer using IO list to build up the output then write it to a file, a socket, or return it from a plug.
# Less Preferred def output do foo = "foo" bar = "bar" foo <> bar <> foo <> bar <> foo <> bar end # Preferred 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
if you do not intend on documenting the module.defmodule SomeModule do @moduledoc false # ... end
-
Separate code after the
@moduledoc
with a blank line.# Bad defmodule SomeModule do @moduledoc """ About the module """ use AnotherModule end # Good defmodule SomeModule do @moduledoc """ About the module """ use AnotherModule end
-
Always reference modules by their full name.
# Bad defmodule AwesomeModule do @moduledoc """ This module contains `ModuleA` """ # ... end # Good 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.
# Bad @doc """ This function is similar to `other_function`. """ def awesome_function do # ... end # Good @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.