Skip to content

Guide for homogeneous coding in projects that require the use of the Elixir programming language.

Notifications You must be signed in to change notification settings

nuu-engineering/Elixir-Style-Conventions

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

49 Commits
 
 
 
 
 
 
 
 

Repository files navigation

NUU Style Conventions: Elixir Programming Language

Guide for homogeneous coding in projects that require the use of the Elixir programming language.

NUU Group Documents

Table of Contents

Guide

Source Code Layout

  1. Avoid lines longer than 80 characters.

  2. Avoid trailing whitespace.

  3. Do not use semicolon to end the line, simply use a line ending.

    # Not preferred
    some_function();
    
    # Preferred
    some_function()
  4. Use Unix-style line endings \n (Operating systems derived from Unix are covered by default, Windows users have to be extra careful that the line endings were not \n\r).

    If you're using Git you might want to add the following configuration setting to protect your project from Windows line endings creeping in:

    git config --global core.autocrlf
  5. Use two spaces per indentation level. No hard tabs.

    # Not preferred (4 spaces)
    def some_function do
        do_something()
    end
    
    # Preferred (2 spaces)
    def some_function do
      do_something()
    end
  6. Use spaces after commas, colons and around operators. Except for unary operators, the operator point and the operator rank (two points). Do not put spaces around matched pairs like brackets, parentheses, etc. Whitespace might be (mostly) irrelevant to the Elixir runtime, but its proper use is the key to writing easily readable code.

    Style rule Operators
    No spaces @ . + - ! ^ & .. ~~~ () [] {}
    Space after , : not
    Spaces around * / + - ++ -- <> in not in |> <<< >>> ~>> <<~ ~> <~ <~> <|> < > <= >= == != =~ === !== && &&& and || ||| or = => :: when <- \\

    ⚠️ The operators + - are overloaded, they exist with one-arity and two-arity. When used with one-arity, no spaces are required. When are used with two-arity, spaces are placed around the operator.

    # Not preferred
    result=2*5
    list =[ 1,<< 2 >> ,3 ]++[4 ,5 ]
    some_string|>String.trim()|>String.split( "-" )
    
    for num<-list,do: num||0
    
    num = - 2
    inverse= ! bool
    Enum . map( list, some_function( ) )
    range = 1 .. 10
    
    def other_function( args,options\\[  ] ),do: some_function( )
    
    # Preferred
    result = 2 * 5
    list = [1, <<2>>, 3] ++ [4, 5]
    some_string |> String.trim() |> String.split("-")
    
    for num <- list, do: num || 0
    
    num = -2
    inverse = !bool
    Enum.map(list, some_function())
    range = 1..10
    
    def other_function(args, options \\ []), do: some_function()
  7. Use blank lines between defs to break up a function into logical paragraphs.

    # Not preferred
    def some_function(some_data) do
      altered_data = Module.function(data)
    end
    def some_function do
      result
    end
    def some_other_function do
      another_result
    end
    
    # Preferred
    def some_function(some_data) do
      altered_data = Module.function(data)
    end
    
    def some_function do
      result
    end
    
    def some_other_function do
      another_result
    end
  8. Run single-line defs that match for the same function together, but separate multiline defs with a blank line.

    # Not preferred
    def some_function(nil), do: {:error, "No Value"}
    
    def some_function([]), do: :ok
    
    def some_function([first | rest]), do: some_function(rest)
    
    # Preferred
    def some_function(nil), do: {:error, "No Value"}
    def some_function([]), do: :ok
    def some_function([first | rest]), do: some_function(rest)
    
    # Also preferred
    def some_function(nil), do: {:error, "No Value"}
    def some_function([]), do: :ok
    
    def some_function([first | rest]) do
      some_function(rest)
    end
  9. If the function head and do: clause are too long to fit on the same line, put do: on a new line, indented one level more than the previous line.

    # Preferred
    def some_function([:foo, :bar, :baz] = args),
      do: Enum.map(args, fn arg -> arg <> " is on a very long line!" end)

    When the do: clause starts on its own line, treat it as a multiline def by separating it with blank lines.

    # Not preferred
    def some_function([]), do: :empty
    def some_function(_),
      do: :very_long_line_here
    
    # Preferred
    def some_function([]), do: :empty
    
    def some_function(_),
      do: :very_long_line_here
  10. If you have more than one multiline def matching the same function, do not use single-line defs.

    # Not preferred
    def some_function(nil), do: {:error, "No Value"}
    def some_function([]), do: :ok
    
    def some_function([first | rest]) do
      some_function(rest)
    end
    
    def some_function([first | rest], opts) do
      some_function(rest, opts)
    end
    
    # Preferred
    def some_function(nil) do
      {:error, "No Value"}
    end
    
    def some_function([]) do
      :ok
    end
    
    def some_function([first | rest]) do
      some_function(rest)
    end
    
    def some_function([first | rest], opts) do
      some_function(rest, opts)
    end
  11. Use the pipe operator |> to chain functions together.

    # Not preferred
    String.strip(String.downcase(some_string))
    
    # Preferred
    some_string |> String.downcase() |> String.strip()
    
    # Multiline pipelines are not further indented.
    some_string
    |> String.downcase()
    |> String.strip()
    
    # Multiline pipelines on the right side of a pattern match should be
    # indented on a new line.
    sanitized_string =
      some_string
      |> String.downcase()
      |> String.strip()

    While this is the preferred method, take into account that copy-pasting multiline pipelines into IEx might result in a syntax error, as IEx will evaluate the first line without realizing that the next line has a pipeline.

  12. Use bare expresions in the first part of a function chain.

    # THE WORST!
    # This actually parses as:
    # String.strip("nope" |> String.downcase())
    String.trim "nope" |> String.downcase()
    
    # Not preferred
    String.trim(some_string) |> String.downcase() |> String.codepoints()
    
    # Preferred
    some_string |> String.trim() |> String.downcase() |> String.codepoints()
  13. Avoid using the pipe operator |> just once.

    # Not preferred
    some_string |> String.downcase()
    
    # Preferred
    String.downcase(some_string)
  14. Use parentheses for one-arity functions when using the pipe operator |>.

    # Not preferred
    some_string |> String.downcase |> String.strip
    
    # Preferred
    some_string |> String.downcase() |> String.strip()
  15. Do not use anonymous functions in pipelines.

    # Not preferred
    some_string
    |> String.trim()
    |> String.downcase()
    |> (fn string -> string <> "_OK" end).()
    |> String.split("-")
    
    # Preferred
    some_string
    |> String.trim()
    |> String.downcase()
    |> review()
    |> String.split("-")
    
    def review(string), do: string <> "_OK"
  16. When case or cond clauses span multiple lines, separate each clause with a blank line.

    # Not preferred
    case arg do
      true ->
        :ok
      false ->
        :error
    end
    
    # Preferred
    case arg do
      true ->
        :ok
    
      false ->
        :error
    end
  17. If a list, tuple, map, or struct spans multiple lines, put each element, as well as the opening and closing brackets, on its own line. Indent each element one level, but not the brackets.

    # Not preferred
    [:first_item, :second_item, :next_item,
    :final_item]
    
    # Preferred
    [
      :first_item,
      :second_item,
      :next_item,
      :final_item
    ]
  18. When assigning a list, tuple, map, or struct, keep the opening bracket on the same line as the assignment.

    # Not preferred
    list =
    [
      :first_item,
      :second_item
    ]
    
    # Preferred
    list = [
      :first_item,
      :second_item
    ]
  19. After a multiline assignment add a blank line as separation.

    # Not preferred
    some_string =
      "Hello"
      |> String.downcase()
      |> String.strip()
    another_string <> some_string
    
    # Preferred
    some_string =
      "Hello"
      |> String.downcase()
      |> String.strip()
    
    another_string <> some_string
  20. Add underscores to decimal literals that have six or more digits.

    # Not preferred
    num = 1000000
    num = 1_500
    
    # Preferred
    num = 1_000_000
    num = 1500
  21. Use uppercase letters when using hex literals.

    # Not preferred
    <<0xef, 0xbb, 0xbf>>
    
    # Preferred
    <<0xEF, 0xBB, 0xBF>>
  22. End each file with a newline.

Syntax

  1. Use parentheses when a def structure has arguments, and omit them when it doesn't.

    # Not preferred
    def some_function arg1, arg2 do
      ...
    end
    
    def some_function() do
      ...
    end
    
    # Preferred
    def some_function(arg1, arg2) do
      ...
    end
    
    def some_function do
      ...
    end
  2. Never put a space between a function name and the opening parenthesis.

    # Not preferred
    f (3 + 2)
    
    # Preferred
    f(3 + 2)
  3. Use always parentheses in function calls, especially inside a pipeline and in functions that do not receive arguments so they can be distinguished from variables. Starting in Elixir 1.4, the compiler will warn you about locations where this ambiguity exists.

    # Not preferred
    f 3
    
    Enum.reduce 1..100, 0, fn x, acc ->
      x + acc
    end
    
    # This actually parses as: rem(2, (3 |> g()))
    2 |> rem 3 |> g
    
    def my_func do
      # Is this a variable or a function call?
      do_stuff
    end
    
    # Preferred
    f(3)
    
    Enum.reduce(1..100, 0, fn(x, acc) ->
      x + acc
    end)
    
    2 |> rem(3) |> g()
    
    def my_func do
      # This is clearly a function call.
      do_stuff()
    end
  4. For macros we see the contrary behaviour. The preferred way is to not use parentheses.

    # Not preferred
    if( valid?(username) ) do
      ...
    end
    
    defmodule( MyApp.Service.TwitterAPI ) do
      use MyApp.Service, social: true
    
      alias MyApp.Service.Helper, as: H
    end
    
    # Preferred
    if valid?(username) do
      ...
    end
    
    defmodule MyApp.Service.TwitterAPI do
      use MyApp.Service, social: true
    
      alias MyApp.Service.Helper, as: H
    end
  5. Use do: syntaxis for single line macros statement.

    # Preferred
    if some_condition, do: # some_stuff
    
  6. If the arguments of a macro make the line too long, indent and align the successive arguments. Place the do: in a new line with one indentation level.

    # Preferred
    with {:ok, foo} <- Keyword.fetch(opts, :foo),
         {:ok, bar} <- Keyword.fetch(opts, :bar),
      do: {:ok, foo, bar}
  7. If the macro has a do block with more than one line, or has an else option, use multiline syntax.

    # Preferred
    with {:ok, foo} <- Keyword.fetch(opts, :foo),
         {:ok, bar} <- Keyword.fetch(opts, :bar) do
      {:ok, foo, bar}
    else
      :error ->
        {:error, :bad_arg}
    end
  8. Never use unless with else. Rewrite these with the positive case first with an if construct.

    # Not preferred
    unless success do
      IO.puts('failure')
    else
      IO.puts('success')
    end
    
    # Preferred
    if success do
      IO.puts('success')
    else
      IO.puts('failure')
    end
  9. Omit the else option in if and unless constructs if else returns nil.

    # Not preferred
    if byte_size(data) > 0, do: data, else: nil
    
    # Preferred
    if byte_size(data) > 0, do: data
  10. Use true as the last condition of the cond special form when you need a clause that always matches.

    # Not preferred
    cond do
      1 + 2 == 5 ->
        "Nope"
    
      1 + 3 == 5 ->
        "Uh, uh"
    
      :else ->
        "OK"
    end
    
    # Preferred
    cond do
      1 + 2 == 5 ->
        "Nope"
    
      1 + 3 == 5 ->
        "Uh, uh"
    
      true ->
        "OK"
    end
  11. Use or, and and not for strictly boolean checks. Use ||, &&, and ! operators only if any of the arguments are non-boolean.

    # Not preferred
    is_atom(name) && name != nil
    is_binary(task) || is_atom(task)
    
    # Preferred
    is_atom(name) and name != nil
    is_binary(task) or is_atom(task)
    line && line != 0
    file || "sample.exs"
  12. Always use the special syntax for keyword lists.

    # Not preferred
    some_value = [{:a, "baz"}, {:b, "qux"}]
    
    # Preferred
    some_value = [a: "baz", b: "qux"]
  13. Omit square brackets from keyword lists whenever they are optional.

    # Not preferred
    some_function(foo, bar, [a: "baz", b: "qux"])
    
    # Preferred
    some_function(foo, bar, a: "baz", b: "qux")

Naming

  1. Use snake case for naming directories and files, for example: lib/my_app/task_server.ex.

  2. Use upper camel case for modules (keep acronyms like HTTP, RFC, XML uppercase).

    # Not preferred
    defmodule Somemodule do
      ...
    end
    
    defmodule Some_Module do
      ...
    end
    
    defmodule SomeXml do
      ...
    end
    
    # Preferred
    defmodule SomeModule do
      ...
    end
    
    defmodule SomeXML do
      ...
    end
  3. Use snake case for atoms, functions and variables.

    # Not preferred
    :"some atom"
    :SomeAtom
    :someAtom
    
    someVar = 5
    
    def someFunction do
      ...
    end
    
    # Preferred
    :some_atom
    
    some_var = 5
    
    def some_function do
      ...
    end
  4. The names of macros (compile-time generated functions that return a boolean value) that can be used within guards should be prefixed with "is_".

    # Preferred
    defmacro is_cool(var) do
      quote do: unquote(var) == "cool"
    end
  5. The names of functions that return a boolean value should have a trailing question mark "?" rather than the "is_" prefix.

    # Preferred
    def cool?(var) do
      ...
    end
  6. The private functions with the same name as public functions should have the "do_" prefix.

    # Preferred
    def sum(list), do: do_sum(list, 0)
    
    defp do_sum([], total), do: total
    defp do_sum([head | tail], total), do: do_sum(tail, head + total)

Comments

  1. Write expressive code and try to convey your program's intention through control-flow, structure and naming.

  2. Place comments above the line they comment on.

    String.first(some_string) # Not preferred
    
    # Preferred
    String.first(some_string)
  3. Use one space between the leading "#" character of the comment and the text of the comment.

    #Not preferred
    String.first(some_string)
    
    # Preferred
    String.first(some_string)
  4. Comments longer than a word are capitalized, and sentences use punctuation. Use one space after periods.

    # Not preferred
    # these lowercase comments are missing punctuation
    
    # Preferred
    # Capitalization example: Use punctuation for complete sentences.

Comment Annotations

  1. The annotation keyword is uppercase, and is followed by a colon and a space, then a note describing the problem.

    # Preferred
    # TODO: Deprecate in v1.5.
    def some_function(arg), do: {:ok, arg}
  2. In cases where the problem is so obvious that any documentation would be redundant, annotations may be left with no note. This usage should be the exception and not the rule.

    start_task()
    
    # FIXME
    Process.sleep(5000)
  3. Use TODO to note missing features or functionality that should be added at a later date

  4. Use FIXME to note broken code that needs to be fixed.

  5. Use OPTIMIZE to note slow or inefficient code that may cause performance problems.

  6. Use HACK to note code smells where questionable coding practices were used and should be refactored away.

  7. Use REVIEW to note anything that should be looked at to confirm it is working as intended.

    # REVIEW: Are we sure this is how the client does X currently?
  8. Use other custom annotation keywords if it feels appropriate, but be sure to document them in your project's README or similar.

Modules

  1. Use one module per file unless the module is only used internally by another module (such as a test).

  2. Use snake case file names for upper camel case module names.

    # Preferred
    # file is called: some_module.ex
    
    defmodule SomeModule do
    end
  3. Represent each level of nesting within a module name as a directory.

    # Preferred
    # file is called: parser/core/xml_parser.ex
    
    defmodule Parser.Core.XMLParser do
    end
  4. Don't put a blank line after defmodule.

    # Not preferred
    defmodule System.Accounts do
    
      import Ecto.Query, warn: false
      ...
    end
    
    # Preferred
    defmodule System.Accounts do
      import Ecto.Query, warn: false
      ...
    end
  5. Put a blank line after module-level code blocks.

  6. List module attributes and directives in the following order:

    1. @moduledoc
    2. @behaviour
    3. use
    4. import
    5. alias
    6. require
    7. defstruct
    8. @type
    9. @module_attribute
    10. @callback
    11. @macrocallback
    12. @optional_callbacks
  7. Add a blank line between each grouping, and sort the terms (like module names) alphabetically. Here's an overall example of how you should order things in your modules:

    # Preferred
    defmodule MyModule do
      @moduledoc """
      An example module
      """
    
      @behaviour MyBehaviour
    
      use GenServer
    
      import Something
      import SomethingElse
    
      alias My.Long.Module.Name
      alias My.Other.Module.Example
    
      require Integer
    
      defstruct name: nil, params: []
    
      @type params :: [{binary, binary}]
    
      @module_attribute :foo
      @other_attribute 100
    
      @callback some_function(term) :: :ok | {:error, term}
    
      @macrocallback macro_name(term) :: Macro.t()
    
      @optional_callbacks macro_name: 1
    
      ...
    end
  8. Use the __MODULE__ pseudo variable when a module refers to itself. This avoids having to update any self-references when the module name changes.

    # Preferred
    defmodule SomeProject.SomeModule do
      defstruct [:name]
    
      def name(%__MODULE__{name: name}), do: name
    end

    If you want a prettier name for a module self-reference, set up an alias.

    # Preferred
    defmodule SomeProject.SomeModule do
      alias __MODULE__, as: SomeModule
    
      defstruct [:name]
    
      def name(%SomeModule{name: name}), do: name
    end
  9. Avoid repeating fragments in module names and namespaces. This improves overall readability and eliminates ambiguous aliases.

    # Not preferred
    defmodule Todo.Todo do
      ...
    end
    
    # Preferred
    defmodule Todo.Item do
      ...
    end

Functions

  1. If a function receives options, they must be grouped into a keyword list as the last argument, and with the operator \\ an empty list will be assigned as default. Each option will have its own variable, wich is assigned the value of the corresponding keyword in the options argument with the Keyword.get method, or a default value in the case of not being provided.

    # Not preferred
    def some_function(arg, opt1 \\ "Default value 1", opt2 \\ 255, opt3 \\ false) do
      ...
    end
    
    # Preferred
    def some_function(arg, options \\ []) do
      opt1 = Keyword.get(options, :opt1, "Default value 1")
      opt2 = Keyword.get(options, :opt2, 255)
      opt3 = Keyword.get(options, :opt3, false)
    
      ...
    end

Documentation

Documentation in Elixir (when read either in IEx with h or generated with ExDoc) uses the Module Attributes @moduledoc and @doc.

  1. Use @moduledoc false if you do not intend on documenting the module.

    # Not preferred
    defmodule SomeModule do
      ...
    end
    
    # Preferred
    defmodule SomeModule do
      @moduledoc false
      ...
    end
  2. Separate code after the @moduledoc with a blank line.

    # Not preferred
    defmodule SomeModule do
      @moduledoc """
      About the module
      """
      use AnotherModule
    end
    
    # Preferred
    defmodule SomeModule do
      @moduledoc """
      About the module
      """
    
      use AnotherModule
    end
  3. Use heredocs strings with markdown for documentation.

    # Not preferred
    defmodule SomeModule do
      @moduledoc "About the module"
    end
    
    defmodule SomeModule do
      @moduledoc """
      About the module
    
      Examples:
      iex> SomeModule.some_function
      :result
      """
    end
    
    # Preferred
    defmodule SomeModule do
      @moduledoc """
      About the module
    
      ## Examples
    
          iex> SomeModule.some_function
          :result
      """
    end

Typespecs

Typespecs are notation for declaring types and specifications, for documentation or for the static analysis tool Dialyzer.

Custom types should be defined at the top of the module with the other directives.

  1. Place @typedoc and @type definitions together, and separate each pair with a blank line.

    # Preferred
    defmodule SomeModule do
      @moduledoc false
    
      @typedoc "The name"
      @type name :: atom
    
      @typedoc "The result"
      @type result :: {:ok, term} | {:error, term}
    
      ...
    end
  2. If a union type is too long to fit on a single line, put each part of the type on a separate line, indented one level past the name of the type.

    # Not preferred
    @type long_union_type :: some_type | another_type | some_other_type |
    a_final_type
    
    # Preferred
    @type long_union_type ::
            some_type
            | another_type
            | some_other_type
            | a_final_type
  3. Name the main type for a module t, for example: the type specification for a struct.

    # Preferred
    defstruct name: nil, params: []
    
    @type t :: %__MODULE__{
            name: String.t() | nil,
            params: Keyword.t()
          }
  4. Place specifications right before the function definition, without separating them by a blank line.

    # Not preferred
    @spec some_function(term) :: result
    
    def some_function(some_data) do
      {:ok, some_data}
    end
    
    # Preferred
    @spec some_function(term) :: result
    def some_function(some_data) do
      {:ok, some_data}
    end

Structs

  1. Use a list of atoms for struct fields that default to nil, followed by the other keywords.

    # Not preferred
    defstruct name: nil, params: nil, active: true
    
    # Preferred
    defstruct [:name, :params, active: true]
  2. Omit square brackets when the argument of a defstruct is a keyword list. Brackets will not be omitted if there is at least one atom in the list.

    # Not preferred
    defstruct [params: [], active: true]
    
    # Preferred
    defstruct params: [], active: true
    
    # Required - Brackets are not optional, with at least one atom in the list.
    defstruct [:name, params: [], active: true]
  3. If a struct definition spans multiple lines, put each element on its own line, keeping the elements aligned.

    # Not preferred
    defstruct foo: "test", bar: true, baz: false,
      qux: false, quux: 1
    
    # Preferred
    defstruct foo: "test",
              bar: true,
              baz: false,
              qux: false,
              quux: 1
  4. If a multiline struct requires brackets, format it as a multiline list:

    # Preferred
    defstruct [
      :name,
      params: [],
      active: true
    ]

Exceptions

  1. Make exception names end with a trailing "Error".

    # Not preferred
    defmodule BadHTTPCode do
      defexception [:message]
    end
    
    defmodule BadHTTPCodeException do
      defexception [:message]
    end
    
    # Preferred
    defmodule BadHTTPCodeError do
      defexception [:message]
    end
  2. Use lowercase error messages when raising exceptions, with no trailing punctuation.

    # Not preferred
    raise ArgumentError, "This is not valid."
    
    # Preferred
    raise ArgumentError, "this is not valid"

Strings

  1. Match strings using the string concatenator rather than binary patterns:

    # Not preferred
    <<"my"::utf8, _rest::bytes>> = "my string"
    
    # Preferred
    "my" <> _rest = "my string"

Testing

  1. When writing ExUnit assertions, be consistent with the order of the expected and actual values under testing. Prefer placing the expected result on the right, unless the assertion is a pattern match.

    # Not preferred - Inconsistent order.
    assert actual_function(1) == true
    assert false == actual_function(2)
    
    # Preferred - Expected result on the right.
    assert actual_function(1) == true
    assert actual_function(2) == false
    
    # Required - The assertion is a pattern match.
    assert {:ok, expected} = actual_function(3)

Languages

Copying

License

Creative Commons License This work is licensed under a Creative Commons Attribution 3.0 Unported License.

Attribution

The structure of this guide, bits of example code, and many of the initial points made in this document were borrowed from Christopher Adams', Alberto Almargo's, Aleksei Magusev's and Credo's Elixir style guides.

About

Guide for homogeneous coding in projects that require the use of the Elixir programming language.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published