Elixir 🧪

Hero image for Elixir 🧪

Linting

The team mainly uses and respects 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 new line.

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")
    

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 line if/unless statements

    if some_condition do
      "some_stuff"
    end
    
    if some_condition, do: "some_stuff"
    
  • Never use unless with else. 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 and do: clause are too long to fit on the same line, then put do: 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
    

Adding prefixes can improve code readability in larger codebases or long function piping.

  • 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

  • Avoid using macros because they are harder to write and understand compared to ordinary Elixir functions.

Macros can lead to implicit code, which is considered a bad style to use when they’re not necessary. For more details, check the official guide.

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
    

Doctest cannot be run for the function that has side-effect e.g. interaction with Database, or involving external API calls.

  • 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
    

Documentation is not necessary for every module and function. Developer who works on the codebase or code reviewers can consider if specific modules or functions are complex enough and the document is needed.

Testing

To test Elixir applications, use ExUnit.

Check here for convention and best practices