Elixir 🧪

Hero image for 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.
  • Use a single empty line to break between statements to organize logical chunks of code.
  • 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 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

Syntax

  • Use the shorthand syntax to group aliases from the same sub-dmodule.
# 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

Functions

  • 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
  • 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}

Modules

  • Order the dependencies of a module in the following grouping order: use, import, and alias, and put an empty line between each type of dependency.
defmodule Payment.Merchants.Merchant do
  use Ecto.Schema

  import Ecto.Changeset

  alias Payment.ReferenceGenerator
end
  • Order the dependencies of a module in alphabetical order
# Bad
alias PaymentService.Verifications
alias PaymentService.Currency
alias PaymentService.TopUps

# Good
alias PaymentService.Currency
alias PaymentService.TopUps
alias PaymentService.Verifications

Documentation

  • Use @moduledoc to document modules.
defmodule Payment.PasswordAuthenticator do
  @moduledoc """
  Authenticate a merchant with email and password
  """
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_bank_partner_detail(String.t()) ::
      {:ok, %BankPartnerDetail{}} | {:error, atom(), String.t()}
def fetch_bank_partner_detail(partner) do
  #...
end