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 (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
    

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

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

    # 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 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
    

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
    alias PaymentService.TopUps
    alias PaymentService.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
    

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