Overall policy:
- Be consistent within a chunk of your changes.
- Be consistent with surrounding code.
- Make use of this (and linked) Style Guides when in doubt.
- Ask and discuss!
We are mostly following Credo's Elixir Style Guide.
Styles marked as preferred in the Credo's guide are mostly used.
If you find yourself lost about coding style, always search in existing code for similarities. Or just ask in a Pull Request or Users ML.
As a side-reading, A community driven style guide for Elixir is also available. We are not compliant with all of it, but it provides good advices.
Note: Although we are mostly following its style guide, we are not completely relying on Credo static analysis.
Typespecs are useful for both documentation purpose and success typing analysis (dialyzer). So it's preferred to explicitly declare typespecs of functions, especially for public ones.
We are utilizing Croma.Defun
series for easy typespec notations.
Example:
@type timed_id_t :: {pos_integer, reference}
defun five_params_fun(key :: v[atom],
some_str :: v[String.t],
key_list :: [atom],
timed_id :: timed_id_t,
state :: v[map]) :: :ok | {:error, term} do
# Do something
end
Also, use v[]
validation where applicable. See Croma.Defun
for details.
Note that validations of arguments by v[]
are disabled when compiling for production environment.
Do not hesitate to put typespecs on complicated private functions too.
For module names we simply use CamelCase even if they contain acronyms.
For instance we use Antikythera.Url
, not Antikythera.URL
.
This is to eliminate exceptional case in our naming rules,
which leads to easier conversions between module aliases and strings.
Module aliases MUST be ordered in their generality; modules with general use cases MUST come above modules with limited use cases.
- External modules
Antikythera
AntikytheraCore
AntikytheraEal
For sub-modules under namespaces listed above, there is no specific order defined.
You SHOULD bundle modules of the same level:
alias Antikythera.{GearName, GearNameStr}
import
ing a module will allow you to use its functions without SomeModule.
prefix.
However it SHOULD NOT be abused because it can lead to name conflicts.
- Use only when you are absolutely sure it is safe and useful there.
- Limit its targets with
:only
option. - Consider limiting its scope by placing it inside a specific scope.
You MUST NOT omit parentheses in function calls.
# Okay
result = zero_arity_function()
result = one_arity_function(arg1)
# Not okay as it gets warned by elixir compiler
result = zero_arity_function
# Not okay (less readable especially when some args are long and not pipe-ready)
result = some_function arg1, arg2, arg3, arg4
# Okay
result = some_function(arg1, arg2, arg3, arg4)
Also you MUST NOT omit parentheses when you define functions. Note that you MAY omit parentheses when invoking macros.
Pipe operator improves code readability when 1st argument is a complex expression because it reorders expressions in the evaluation order.
# You have to read inside-out, as function arguments are evaluated before function invocation
Enum.sort(Enum.filter(list, &some_predicate/1))
# Much readable as you can now naturally follow the evaluation order from left to right
Enum.filter(list, &some_predicate/1) |> Enum.sort()
Also pipe operator makes separation between "data" and "its transformation" clearer.
list
|> Enum.map(some_func)
|> Enum.filter(some_predicate)
|> Enum.reduce(acc0, some_reducer)
If you have neither of the above benefits in mind, you SHOULD NOT use pipe operator.
# absurd
keys = some_map |> Map.keys()
# simple enough
keys = Map.keys(some_map)