Elixir - SolarisJapan/lunaris-wiki GitHub Wiki
Best Practice and conventions in Elixir
Even though Elixir covers many code style conventions in the integrated formatter, there are some cases where the best practice or convention is not immediately obvious. In this wiki we will link to existing resources if possible and only add more information if there are any Lunaris specific conventions.
Some topics might be strongly opinionated, so take everything with a grain of salt and apply common sense before blindly following any advice you find in this wiki or online.
Code Style
Code style is mostly covered in the language itself or by community driven style guides.
Resources
Atom and the Elixir formatter
If you are using atom, I strongly recommend the atom elixir formatter package with the format on save
option set to Only if projects includes a .formatter file in its root
(to prevent breaking in projects without a .formatter
file). The formatter file for phoenix looks like this
# .formatter.exs
[
import_deps: [:ecto, :phoenix],
inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"],
subdirectories: ["priv/*/migrations"]
]
and should be included automatically when you create a new phoenix project with mix phx.new
.
The formatter file for a regular mix project is even simpler
# formatter.exs
[
inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
Testing
A 100% test coverage is usually more decremental than useful in my opinion (especially in phoenix projects where we have many modules only using macros / functions from dependencies which are tested in their respective projects).
Writing higher level tests before the implementation can help you find a reasonable structure and module naming
Resources
- elixir school: Testing
- Jose Valim on mocking vs mocks
- Mox, a library to create mocks for predefined behaviours
- Testing in Phoenix
- Testing in this Wiki
- Test Factories in this Wiki
- Doctests
Documentation
Elixir has excellent support for code documentation backed into the language out of the box! In fact, when you visit the documentation on hexdocs.pm
for any given package (and also Elixir itself), it uses the integrated documentation tools. You can even write tests in your documentation!
Please do not neglect writing documentation. Sometimes writing documentation for a module or function before starting the implementation can help setting natural boundaries / limit the concerns a function or module should have.
There are some packages with excellent documentation, if you want some inspiration, you can look into the code for ecto.
A few things you want to document
@moduledoc
Describe what this module is used for, give some examples and try to explain in what context this module is supposed to be used.
@doc
Again, at least give some examples. If you have multiple function heads, please don't forget to explain what the difference is between the usage with different arguments. If there is anything to keep in mind when using the function, don't forget to mention it.
COMBAK
, TODO
, BUG
, INFO
etc for "inline" documentation
Use There atom packages to highlight comment keywords, and it also makes it easier to find todos and bugs later on.
Use the documentation
Documentation websites can be generated, but in general it's good enough to have the documentation in your code. When done correctly, it also gives you some useful tools for usage in iex.
# iex -S mix
iex> h Ecto.Query
# Output:
#
# Defines a repository.
#
# A repository maps to an underlying data store, controlled by the adapter. For
# example, Ecto ships with a Postgres adapter that stores data into a PostgreSQL
# database.
# ...
iex> Ecto.Query.__info__ :functions
[
__struct__: 0,
__struct__: 1,
exclude: 2,
first: 1,
first: 2,
has_named_binding?: 2,
last: 1,
last: 2,
reverse_order: 1,
subquery: 1,
subquery: 2
]
iex> h Ecto.Query.exclude
# Output:
#
# Resets a previously set field on a query.
#
# It can reset many fields except the query source (from). When excluding a
# :join, it will remove all types of joins. If you prefer to remove a single type
# of join, please see paragraph below.
#
# ## Examples
# ...
Resources
Debugging
As a central part of our daily work, it is important to know what Elixir has to offer in terms of debugging tools.
If you are coming from a Ruby background, debugging might look quite different from what you are used to, as there is way less runtime magic available, making the usage of some tools "harder" - or more explicit.
If you are used to a language that has an IDE that lets you set breakpoints etc (like Java), debugging in Elixir might not come as natural to you, and you have to adjust to having to do more coding, than clicking.
Nevertheless, there are great tools for Elixir.
Resources
- Plataformatec blogpost on debugging
- Official Elixir Guide
- Elixir School
- Debugging talk (Video)
- IEx.pry
A word on Dialyzer
While it is a great tool to make Elixir a little "type save - ish", it does not perform that great in Phoenix projects, and is hard to implement if not done right from the start of the project. However, the typespec notations can be used as a documentation tool, without running Dialyzer.
Debugging in Phoenix
Keep in mind that you can run a project in iex iex -S mix
, and since the phoenix server is simply a mix task, you can start it as an interactive console iex -S mix phx.server
. You can set a breakpoint like this
# require pry
require IEx
# then call IEx.pry()
defmodule Debug.PageController do
use Debug.Web, :controller
def index(conn, _params) do
IEx.pry
render conn, "index.html"
end
end
The problem with this approach is, that log messages will mess with your interactive session, parallel requests might become a problem. You can set a "lock" using ets
to avoid elixir trying to open multiple breakpoints.
defmodule MyAppWeb.PryCheck do
def pry_check? do
unless :undefined == :ets.whereis(:pry_check) do
with {"pry", engagement} <-
:ets.lookup(:pry_check, "pry")
|> List.first(),
do: engagement
end
end
def engage_pry do
if :undefined == :ets.whereis(:pry_check) do
:ets.new(:pry_check, [:set, :protected, :named_table])
end
:ets.insert(:pry_check, {"pry", true})
end
def disengage_pry do
unless :undefined == :ets.whereis(:pry_check) do
:ets.insert(:pry_check, {"pry", false})
end
end
end
These functions can then be used in the following manner:
import MyAppWeb.PryCheck
unless pry_check? do
engage_pry
require IEx
IEx.pry()
disengage_pry
end