This guide is a quick set of guidelines for reading or writing elixir code. It's designed to get you through stuff that is different from Elixir relative to other programming languages you might be used to, and which you might miss while learning elixir.
asdf is a very convenient verison manager that we use for managing elixir and erlang versions.
https://asdf-vm.com/guide/getting-started.html
run the following commands:
asdf install erlang 26.0.2
asdf install elixir 1.15.4-otp-26
asdf global erlang 26.0.2
asdf global elixir 1.15.4-otp-26
(ubuntu) sudo apt-get install direnv
Then hook it into your shell: https://direnv.net/docs/hook.html
Installation instructions here: https://brew.sh/
asdf is a very convenient verison manager that we use for managing elixir and erlang versions.
https://asdf-vm.com/guide/getting-started.html
run the following commands:
asdf plugin add erlang
asdf plugin add elixir
asdf install erlang 26.0.2
asdf install elixir 1.15.5-otp-26
asdf global erlang 26.0.2
asdf global elixir 1.15.5-otp-26
brew install direnv
Then follow the directions to add direnv to your ~/.zshrc
.
If you're comfortable with Jupyter-style tooling (it's also a good way to get into playing with elixir quickly), you might want to use livebook to play around with Elixir: https://livebook.dev/
run script | interactive terminal | |
---|---|---|
stdlib only | elixir script.exs | iex |
project | mix run script.exs | iex -S mix |
Note: capital S in iex -S mix
Most important thing to remember is that every line of code in elixir is an expression (that means, that every line has a return value).
You can't rebind variable inside of an if expression and expect it to change the variable on the outside of the if expression.
This doesn't work the way you expect:
x = 10
if true do
x = x + 10
end
x # ==> 10
In order to rebind the value of x, you will need to take advantage of the if being an expression
x = 10
x = if true do
x + 10
end
x # ==> 20
Note that this isn't really awesome either because the fallthrough of if without an else is nil
x = 10
x = if false do
x + 10
end
x # ==> nil
You probably want this
x = 10
x = if condition do
x + 10
else
x
end
x # ==> 10 or 20, depending on condition
But in any case, you probably should assign to a variable with a different name because the intent is clearer
x = 10
new_var = if (condition) do
x + 10
else
x
end
The rules for if
apply to cond
, case
, with
as well. Avoid unless
.
You probably don't need receive
.
new_value = cond do
condition_1 -> choice_1
condition_2 -> choice_2
end
Note that the default fallthrough on cond is true
.
The previous code will crash if neither condition is met, this will not:
new_value = cond do
condition_1 -> choice_1
condition_2 -> choice_2
true -> fallthrough_choice
end
new_value = case value do
{:ok, match} -> choice_1
{:error, error} -> choice_2
end
The default fallthrough on case is _
which is "match anything". The
above code fails if none of the matches succeeds, the following does not:
new_value = case value do
{:ok, match} -> choice_1
{:error, error} -> choice_2
_ -> fallthrough_choice
end
new_value = with {:ok, result1} <- operation1(),
{:ok, result2} <- operation2(result1) do
result2
end
new_value # ==> maybe "result2"
Note that in the previous code if operation1 doesn't match {:ok, result1}
, then
new_value will get the failed value at the point of fallthrough.
new_value = with {:ok, result1} <- operation1(),
{:ok, result2} <- operation2(result1) do
result2
else
{:error, reason} -> raise "failed with #{reason}"
end
new_value # ==> maybe "result2"
Don't struggle too hard with with
. If you are finding you have to write very
complicated else clauses, simply do it as nested case
expressions wrapped in
their own functions.
Also, don't be afraid to crash. Especially don't be afraid to crash when writing tests.
for
is not a loop, it's a comprehension, use it only to build lists.
These are roughly equivalent:
result = for number <- 1..10, rem(number, 2) == 1, do: number * 3
result # <== [3, 9, 15, 21, 27]
result = [number * 3 for number in range(1,10) if number % 2 == 1]
...?
is the convention of how to name a boolean functionis_
functions should be guards. See: https://hexdocs.pm/elixir/main/patterns-and-guards.html#guards Note: guards are a feature that will generate a boolean function that matches the guards as well as a macro that can be used to generate code that can be run where guard clauses are allowed.
- functions named
get
returnnil
if the retrieval doesn't find anything [0]. - functions named
fetch
return:error
or{:error, reason}
if the retrieval doesn't find anything and{:ok, result}
it if does. - functions named
fetch!
are likeget
but raise if the retrieval doesn't find anything.
[0] : obviously, this doesn't apply to functions that are called get
referring to HTTP get
method.
This is a syntactic transform.
[foo: <bar>]
is syntactic sugar for [{:foo, <bar>}]
Note that you can build lists using this:
[1, 2, 3, foo: "bar"]
is syntactic sugar for [1, 2, 3, {:foo, "bar"}]
, but only
the last items can be aggregated as a list, this is illegal: [foo: "bar", 1, 2]
trailing keyword lists are grouped as a single list.
my_function(1, 2, foo: 3, bar: 4)
is equivalent to: my_function(1, 2, [{:foo, 3}, {:bar, 4}])
Also this is only allowed to be at the end:
my_function(1, foo: 3, bar: 4, 5)
is illegal. You must render it as: my_function(1, [foo: 3, bar: 4], 5)
this creates maps with atom keys:
%{foo: "bar"}
is equivalent to %{:foo => "bar"}
they are only allowed to be the last specified k/v pairs. This is legal:
`%{"bar" => "baz", foo: "bar"}
while this is illegal:
%{foo: "bar", "bar" => "baz"}
Of course, do be careful as:
%{"foo" => "bar"} != %{foo: "bar"}
def my_function(param, other \\ :default), do: :whatever
Note if you have multiple function heads, you need to do the following:
def wrap(param, other \\ :tuple)
def wrap(param, :tuple), do: {param}
def wrap(param, :list), do: [param]
aliases are lexically scoped shortcuts.
MyAlias
by default is equal to the atom :"Elixir.MyAlias"
the alias
keyword changes subsequent use of the that alias until the end of the lexical scope
alias Foo.MyAlias
makes MyAlias
equal to the atom :"Elixir.Foo.MyAlias"
until another alias
redefines it or the lexical scope is exited.
Avoid the following, it makse global search-and-replace a pain in the butt:
alias Something.SubAlias, as: AsAlias
Also avoid the following for the same reason:
alias Something.{Foo, Bar, Baz}
The operators and
, or
and not
are only valid for boolean values and will crash if supplied
anything besides true
or false
. They also short-circuit.
The operators &&
, ||
, !
are valid for any term, and are treated as false if the operands are
false
or nil
. They short-circuit.
In general, it's better to use the boolean-only values. Crashing is better than propagating an unexpected type.
This is syntactic sugar and causes no performance differences.
For anonymous functions, you will sometimes see them represented as &(&1 + 10)
. This is
shorthand for fn arg1 -> arg1 + 10 end
. Note that the arguments are one-indexed.
You may also have functions with multiple arguments: &(&1 + &2)
is equivalent to
fn arg1, arg2 -> arg1 + arg2 end
skipping numbers is not allowed, so these are
invalid: &(&2 + 1)
and &(&1 + &3)
There is no way to represent an anonymous function with no arguments using this syntax, so for
those you must do fn -> ... end
If you want to convert a named function to an anonymous function, you can use /arity
suffix.
For a local function (function in the same module), you can do &my_local_function/2
. For a
remote function, you can do &MyModule.some_function/2
.
Anonymous functions are called using function_name.(args...)
my_fun = &(&1 + 10)
my_fun.(20) # --> 30
Elixir has sigils
which are special macros you can use to to imbue strings with
special meanings. Generally, capitalizing the sigil means that the interior string is not interpolated, and
a lower-case sigil means that the interior string is interpolated.
Sigils have many options for their delimiters, they are all equivalent.
~S'text'
is the same as~S"text"
and~S(text)
and~S[text]
and~S|text|
and~S/text/
and~S{text}
and~S<text>
. Multiline text (triple quotes) are also allowed.- You should pick which sigil delimiter to use based on the following criteria:
- if the enclosed text has one of the characters you'd like to use then pick a different sigil.
- if applicable, pick a sigil that looks like the resulting datatype.
~s"..."
turns it into a string. Equivalent to just the double quotes, so almost never used, unless you have tons of double quotes in the string you want~S"..."
as above, but not interpolated or escaped~c'...'
turns it into a charlist (see below). Equivalent to just single quotes~C'...'
as above, but not interpolated or escaped~w[foo bar baz]
wordlist, of strings, broken on whitespace. Produces["foo", "bar", "baz"]
~w[foo bar baz]c
wordlist, of charlists, broken on whitespace. Produces[~C'foo', ~C'bar', ~C'baz']
~w[foo bar baz]a
wordlist, of atoms, broken on whitespace. Produces[:foo, :bar, :baz]
~r/some-regex/
produces a regex.
If you do frontend work you will see ~H
which means "this contains instrumented html (think JSX)" and
generally the included library will parse the html and the instrumented functions and do compile-time checks
on the soundness of the contents.
-
Lists are singly linked lists. This is the most efficient way of doing a "copy-on-write" style list so that functions that need to modify the list don't affect the list in the function that called them. The following consequences hold:
-
Access, modifying, and appending to the front of the list is inexpensive and fast
-
Access and modifying to the rear of the list is expensive and discouraged. Especially think twice before using the
++
operator. -
Be familiar with the meanings of
[a | b]
both as a way to modify an existing list and create a new one, as well as a way to pattern match against existing lists.
You can't easily access data in a list in the way that you're used to, for example:
value = [1, 2, 3] value[2] # <== this doesn't work Enum.at(value, 2) # <== this does workthis is because elixir wants to discourage this because bracket notation implies O(1) access and the bracket notation hides the cost of flipping through the array.
- Almost never use the
--
operator, this has O(NxM) cost.
In Erlang, charlists are literally 'lists of utf-codepoints' and are represented in Elixir as e.g: 'abc'
which
is equivalent to [97, 98, 99]
In Elixir, strings are usually 'binaries' which are contiguous memory regions of bytes. this is represented
in elixir as "abc"
which is equivalent to <<97, 98, 99>>
.
Note that if a binary have a non-ascii-printable byte, it will not be able to use the double-quoted representation.
For example, <<255, 97, 98>>
can't be turned into something double quotes
Note that if a charlist ONLY has ascii-printable bytes, it will be eagerly represented as a charlist. So you might get surprised:
Enum.map([10, 11, 12], &(&1 + 70))
gives you 'PQR'
which may be surprising to you. Note that 'PQR' == [80, 81, 82]
.
If you're using a more advanced version of Elixir (since 1.15), it will be rendered ~c"PQR"
.
While legal, avoid using some_map[key]
Access operations for maps and keyword lists, unless you deliberately
want to support both maps and keyword lists. This should be very rare.
A big footgun. While most matching operations are "must match exactly", map matching is not.
%{foo: bar} = %{foo: 5}
bar # ==> 5
%{foo: bar} = %{foo: 5, baz: 6} # also legal
The biggest place where this can get tricky is:
def foo(arg1, %{} = arg2), do: ....
this does not match arg2
with "empty map". If you need to match against
empty map, do:
def foo(arg1, arg2) when arg2 == %{}, do: ....
Probably the most useful modules are Enum
and Map
modules.
Enum.flat_map/2
Enum.map/2
Enum.reduce/3
List.flatten/1
Map.new/1
Map.new/2
Map.get/2
Map.fetch/2
Map.fetch!/2
Map.update!/3
Map.replace!/3
Map.put/3
- Jason (JSON encoding/decoding): https://hexdocs.pm/jason/Jason.html
- Req (HTTP client): https://hexdocs.pm/req/Req.html
- Mox (mocking for tests): https://hexdocs.pm/mox/Mox.html