Skip to content

lexmag/elixir-style-guide

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

46 Commits
 
 

Repository files navigation

Elixir Style Guide

A programmer does not primarily write code; rather, he primarily writes to another programmer about his problem solution. The understanding of this fact is the final step in his maturation as technician.

— What a Programmer Does, 1967

Table of Contents

The following section are automatically applied by the code formatter in Elixir v1.6 and listed here only for documentation purposes:

Linting

  • Favor the pipeline operator |> to chain function calls together. [link]

    # Bad
    String.downcase(String.strip(input))
    
    # Good
    input |> String.strip() |> String.downcase()

    For a multi-line pipeline, place each function call on a new line, and retain the level of indentation.

    input
    |> String.strip()
    |> String.downcase()
    |> String.slice(1, 3)
  • Avoid needless pipelines like the plague. [link]

    # Bad
    result = input |> String.strip()
    
    # Good
    result = String.strip(input)
  • Don't use anonymous functions in pipelines. [link]

    # Bad
    sentence
    |> String.split(~r/\s/)
    |> (fn words -> [@sentence_start | words] end).()
    |> Enum.join(" ")
    
    # Good
    split_sentence = String.split(sentence, ~r/\s/)
    Enum.join([@sentence_start | split_sentence], " ")

    Consider defining private helper function when appropriate:

    # Good
    sentence
    |> String.split(~r/\s/)
    |> prepend(@sentence_start)
    |> Enum.join(" ")
  • Never use unless with else. Rewrite these with the positive case first. [link]

    # Bad
    unless Enum.empty?(coll) do
      :ok
    else
      :error
    end
    
    # Good
    if Enum.empty?(coll) do
      :error
    else
      :ok
    end
  • Omit the else option in if and unless constructs if else returns nil. [link]

    # Bad
    if byte_size(data) > 0, do: data, else: nil
    
    # Good
    if byte_size(data) > 0, do: data
  • If you have an always-matching clause in the cond special form, use true as its condition. [link]

    # Bad
    cond do
      char in ?0..?9 ->
        char - ?0
    
      char in ?A..?Z ->
        char - ?A + 10
    
      :other ->
        char - ?a + 10
    end
    
    # Good
    cond do
      char in ?0..?9 ->
        char - ?0
    
      char in ?A..?Z ->
        char - ?A + 10
    
      true ->
        char - ?a + 10
    end
  • Never use ||, &&, and ! for strictly boolean checks. Use these operators only if any of the arguments are non-boolean. [link]

    # Bad
    is_atom(name) && name != nil
    is_binary(task) || is_atom(task)
    
    # Good
    is_atom(name) and name != nil
    is_binary(task) or is_atom(task)
    line && line != 0
    file || "sample.exs"
  • Favor the binary concatenation operator <> over bitstring syntax for patterns matching binaries. [link]

    # Bad
    <<"https://", _rest::bytes>> = input
    <<first::utf8, rest::bytes>> = input
    
    # Good
    "https://" <> _rest = input
    <<first::utf8>> <> rest = input

Naming

  • Use snake_case for functions, variables, module attributes, and atoms. [link]

    # Bad
    :"no match"
    :Error
    :badReturn
    
    fileName = "sample.txt"
    
    @_VERSION "0.0.1"
    
    def readFile(path) do
      # ...
    end
    
    # Good
    :no_match
    :error
    :bad_return
    
    file_name = "sample.txt"
    
    @version "0.0.1"
    
    def read_file(path) do
      # ...
    end
  • Use CamelCase for module names. Keep uppercase acronyms as uppercase. [link]

    # Bad
    defmodule :appStack do
      # ...
    end
    
    defmodule App_Stack do
      # ...
    end
    
    defmodule Appstack do
      # ...
    end
    
    defmodule Html do
      # ...
    end
    
    # Good
    defmodule AppStack do
      # ...
    end
    
    defmodule HTML do
      # ...
    end
  • The names of predicate functions (functions that return a boolean value) should have a trailing question mark ? rather than a leading has_ or similar. [link]

    # Bad
    def is_leap(year) do
      # ...
    end
    
    # Good
    def leap?(year) do
      # ...
    end

    Always use a leading is_ when naming guard-safe predicate macros.

    defmacro is_date(month, day) do
      # ...
    end
  • Use snake_case for naming directories and files, for example lib/my_app/task_server.ex. [link]

  • Avoid using one-letter variable names. [link]

Comments

Remember, good code is like a good joke: It needs no explanation.

— Russ Olsen

  • Use code comments only to communicate important details to another person reading the code. For example, a high-level description of the algorithm being implemented or why certain critical decisions, such as optimization or business rules, were made. [link]

  • Avoid superfluous comments. [link]

    # Bad
    String.first(input) # Get first grapheme.

Modules

  • Use a consistent structure when calling use/import/alias/require: call them in this order and group multiple calls to each of them. [link]

    use GenServer
    
    import Bitwise
    import Kernel, except: [length: 1]
    
    alias Mix.Utils
    alias MapSet, as: Set
    
    require Logger
  • Use the __MODULE__ pseudo-variable to reference the current module. [link]

    # Bad
    :ets.new(Kernel.LexicalTracker, [:named_table])
    GenServer.start_link(Module.LocalsTracker, nil, [])
    
    # Good
    :ets.new(__MODULE__, [:named_table])
    GenServer.start_link(__MODULE__, nil, [])

Regular Expressions

  • Regular expressions are the last resort. Pattern matching and the String module are things to start with. [link]

    # Bad
    Regex.run(~r/#(\d{2})(\d{2})(\d{2})/, color)
    Regex.match?(~r/(email|password)/, input)
    
    # Good
    <<?#, p1::2-bytes, p2::2-bytes, p3::2-bytes>> = color
    String.contains?(input, ["email", "password"])
  • Use non-capturing groups when you don't use the captured result. [link]

    ~r/(?:post|zip )code: (\d+)/
  • Be careful with ^ and $ as they match start and end of the line respectively. If you want to match the whole string use: \A and \z (not to be confused with \Z which is the equivalent of \n?\z). [link]

Structs

  • When calling defstruct/1, don't explicitly specify nil for fields that default to nil. [link]

    # Bad
    defstruct first_name: nil, last_name: nil, admin?: false
    
    # Good
    defstruct [:first_name, :last_name, admin?: false]

Exceptions

  • Make exception names end with a trailing Error. [link]

    # Bad
    BadResponse
    ResponseException
    
    # Good
    ResponseError
  • Use non-capitalized error messages when raising exceptions, with no trailing punctuation. [link]

    # Bad
    raise ArgumentError, "Malformed payload."
    
    # Good
    raise ArgumentError, "malformed payload"

    There is one exception to the rule - always capitalize Mix error messages.

    Mix.raise("Could not find dependency")

ExUnit

  • When asserting (or refuting) something with comparison operators (such as ==, <, >=, and similar), put the expression being tested on the left-hand side of the operator and the value you're testing against on the right-hand side. [link]

    # Bad
    assert "héllo" == Atom.to_string(:"héllo")
    
    # Good
    assert Atom.to_string(:"héllo") == "héllo"

    When using the match operator =, put the pattern on the left-hand side (as it won't work otherwise).

    assert {:error, _reason} = File.stat("./non_existent_file")

Formatting

The rules below are automatically applied by the code formatter in Elixir v1.6. They are provided here for documentation purposes and for those maintaining older codebases.

Whitespace

Whitespace might be (mostly) irrelevant to the Elixir compiler, but its proper use is the key to writing easily readable code.

  • Avoid trailing whitespaces. [link]

  • End each file with a newline. [link]

  • Use two spaces per indentation level. No hard tabs. [link]

    # Bad
    def register_attribute(name, opts) do
        register_attribute(__MODULE__, name, opts)
    end
    
    # Good
    def register_attribute(name, opts) do
      register_attribute(__MODULE__, name, opts)
    end
  • Use a space before and after binary operators. Use a space after commas ,, colons :, and semicolons ;. Do not put spaces around matched pairs like brackets [], braces {}, and so on. [link]

    # Bad
    sum = 1+1
    [first|rest] = 'three'
    {a1,a2} = {2 ,3}
    Enum.join( [ "one" , << "two" >>, sum ])
    
    # Good
    sum = 1 + 2
    [first | rest] = 'three'
    {a1, a2} = {2, 3}
    Enum.join(["one", <<"two">>, sum])
  • Use no spaces after unary operators and inside range literals. The only exception is the not operator: use a space after it. [link]

    # Bad
    angle = - 45
    ^ result = Float.parse("42.01")
    
    # Good
    angle = -45
    ^result = Float.parse("42.01")
    2 in 1..5
    not File.exists?(path)
  • Use spaces around default arguments \\ definition. [link]

    # Bad
    def start_link(fun, options\\[])
    
    # Good
    def start_link(fun, options \\ [])
  • Do not put spaces around segment options definition in bitstrings. [link]

    # Bad
    <<102 :: unsigned-big-integer, rest :: binary>>
    <<102::unsigned - big - integer, rest::binary>>
    
    # Good
    <<102::unsigned-big-integer, rest::binary>>
  • Use one space between the leading # character of the comment and the text of the comment. [link]

    # Bad
    #Amount to take is greater than the number of elements
    
    # Good
    # Amount to take is greater than the number of elements
  • Always use a space before -> in 0-arity anonymous functions. [link]

    # Bad
    Task.async(fn->
      ExUnit.Diff.script(left, right)
    end)
    
    # Good
    Task.async(fn ->
      ExUnit.Diff.script(left, right)
    end)

Indentation

  • Indent the right-hand side of a binary operator one level more than the left-hand side if left-hand side and right-hand side are on different lines. The only exceptions are when in guards and |>, which go on the beginning of the line and should be indented at the same level as their left-hand side. Do this also for binary operators when assigning. [link]

    # Bad
    
    "No matching message.\n" <>
    "Process mailbox:\n" <>
    mailbox
    
    message =
      "No matching message.\n" <>
      "Process mailbox:\n" <>
      mailbox
    
    input
      |> String.strip()
      |> String.downcase()
    
    defp valid_identifier_char?(char)
      when char in ?a..?z
        when char in ?A..?Z
        when char in ?0..?9
        when char == ?_ do
      true
    end
    
    defp parenless_capture?({op, _meta, _args})
         when is_atom(op) and
         atom not in @unary_ops and
         atom not in @binary_ops do
      true
    end
    
    # Good
    
    "No matching message.\n" <>
      "Process mailbox:\n" <>
      mailbox
    
    message =
      "No matching message.\n" <>
        "Process mailbox:\n" <>
        mailbox
    
    input
    |> String.strip()
    |> String.downcase()
    
    defp valid_identifier_char?(char)
         when char in ?a..?z
         when char in ?A..?Z
         when char in ?0..?9
         when char == ?_ do
      true
    end
    
    defp parenless_capture?({op, _meta, _args})
         when is_atom(op) and
                atom not in @unary_ops and
                atom not in @binary_ops do
      true
    end
  • Use the indentation shown below for the with special form: [link]

    with {year, ""} <- Integer.parse(year),
         {month, ""} <- Integer.parse(month),
         {day, ""} <- Integer.parse(day) do
      new(year, month, day)
    else
      _ ->
        {:error, :invalid_format}
    end

    Always use the indentation above if there's an else option. If there isn't, the following indentation works as well:

    with {:ok, date} <- Calendar.ISO.date(year, month, day),
         {:ok, time} <- Time.new(hour, minute, second, microsecond),
         do: new(date, time)
  • Use the indentation shown below for the for special form: [link]

    for {alias, _module} <- aliases_from_env(server),
        [name] = Module.split(alias),
        starts_with?(name, hint),
        into: [] do
      %{kind: :module, type: :alias, name: name}
    end

    If the body of the do block is short, the following indentation works as well:

    for partition <- 0..(partitions - 1),
        pair <- safe_lookup(registry, partition, key),
        into: [],
        do: pair
  • Avoid aligning expression groups: [link]

    # Bad
    module = env.module
    arity  = length(args)
    
    def inspect(false), do: "false"
    def inspect(true),  do: "true"
    def inspect(nil),   do: "nil"
    
    # Good
    module = env.module
    arity = length(args)
    
    def inspect(false), do: "false"
    def inspect(true), do: "true"
    def inspect(nil), do: "nil"

    The same non-alignment rule applies to <- and -> clauses as well.

  • Use a single level of indentation for multi-line pipelines. [link]

    input
    |> String.strip()
    |> String.downcase()
    |> String.slice(1, 3)

Term representation

  • Add underscores to decimal literals that have six or more digits. [link]

    # Bad
    num = 1000000
    num = 1_500
    
    # Good
    num = 1_000_000
    num = 1500
  • Use uppercase letters when using hex literals. [link]

    # Bad
    <<0xef, 0xbb, 0xbf>>
    
    # Good
    <<0xEF, 0xBB, 0xBF>>
  • When using atom literals that need to be quoted because they contain characters that are invalid in atoms (such as :"foo-bar"), use double quotes around the atom name: [link]

    # Bad
    :'foo-bar'
    :'atom number #{index}'
    
    # Good
    :"foo-bar"
    :"atom number #{index}"
  • When dealing with lists, maps, structs, or tuples whose elements span over multiple lines and are on separate lines with regard to the enclosing brackets, it's advised to not use a trailing comma on the last element: [link]

    [
      :foo,
      :bar,
      :baz
    ]

Parentheses

  • Parentheses are a must for local or imported zero-arity function calls. [link]

    # Bad
    pid = self
    import System, only: [schedulers_online: 0]
    schedulers_online
    
    # Good
    pid = self()
    import System, only: [schedulers_online: 0]
    schedulers_online()

    The same should be done for remote zero-arity function calls:

    # Bad
    Mix.env
    
    # Good
    Mix.env()

    This rule also applies to one-arity function calls (both local and remote) in pipelines:

    # Bad
    input
    |> String.strip
    |> decode
    
    # Good
    input
    |> String.strip()
    |> decode()
  • Never wrap the arguments of anonymous functions in parentheses. [link]

    # Bad
    Agent.get(pid, fn(state) -> state end)
    Enum.reduce(numbers, fn(number, acc) ->
      acc + number
    end)
    
    # Good
    Agent.get(pid, fn state -> state end)
    Enum.reduce(numbers, fn number, acc ->
      acc + number
    end)
  • Always use parentheses around arguments to definitions (such as def, defp, defmacro, defmacrop, defdelegate). Don't omit them even when a function has no arguments. [link]

    # Bad
    def main arg1, arg2 do
      # ...
    end
    
    defmacro env do
      # ...
    end
    
    # Good
    def main(arg1, arg2) do
      # ...
    end
    
    defmacro env() do
      # ...
    end
  • Always use parens on zero-arity types. [link]

    # Bad
    @spec start_link(module, term, Keyword.t) :: on_start
    
    # Good
    @spec start_link(module(), term(), Keyword.t()) :: on_start()

Layout

  • Use one expression per line. Don't use semicolons (;) to separate statements and expressions. [link]

    # Bad
    stacktrace = System.stacktrace(); fun.(stacktrace)
    
    # Good
    stacktrace = System.stacktrace()
    fun.(stacktrace)
  • When assigning the result of a multi-line expression, begin the expression on a new line. [link]

    # Bad
    {found, not_found} = files
                         |> Enum.map(&Path.expand(&1, path))
                         |> Enum.partition(&File.exists?/1)
    
    prefix = case base do
               :binary -> "0b"
               :octal -> "0o"
               :hex -> "0x"
             end
    
    # Good
    {found, not_found} =
      files
      |> Enum.map(&Path.expand(&1, path))
      |> Enum.partition(&File.exists?/1)
    
    prefix =
      case base do
        :binary -> "0b"
        :octal -> "0o"
        :hex -> "0x"
      end
  • When writing a multi-line expression, keep binary operators at the end of each line. The only exception is the |> operator (which goes at the beginning of the line). [link]

    # Bad
    
    "No matching message.\n"
      <> "Process mailbox:\n"
      <> mailbox
    
    input |>
      String.strip() |>
      decode()
    
    # Good
    "No matching message.\n" <>
      "Process mailbox:\n" <>
      mailbox
    
    input
    |> String.strip()
    |> decode()

License

This work was created by Aleksei Magusev and is licensed under the CC BY 4.0 license.

Creative Commons License

Credits

The structure of the guide and some points that are applicable to Elixir were taken from the community-driven Ruby coding style guide.