Skip to content

Instantly share code, notes, and snippets.

@ityonemo
Last active September 15, 2023 20:41
Show Gist options
  • Save ityonemo/a0dc304f34f878969ac8bd1fdcef53fa to your computer and use it in GitHub Desktop.
Save ityonemo/a0dc304f34f878969ac8bd1fdcef53fa to your computer and use it in GitHub Desktop.
Elixir-Guide

Elixir Guide

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.

Up and running

Linux

Install asdf

asdf is a very convenient verison manager that we use for managing elixir and erlang versions.

https://asdf-vm.com/guide/getting-started.html

Install Elixir

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

Install direnv

(ubuntu) sudo apt-get install direnv

Then hook it into your shell: https://direnv.net/docs/hook.html

MacOS

Install homebrew

Installation instructions here: https://brew.sh/

Install asdf

asdf is a very convenient verison manager that we use for managing elixir and erlang versions.

https://asdf-vm.com/guide/getting-started.html

Install Elixir

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

Install direnv

brew install direnv

Then follow the directions to add direnv to your ~/.zshrc.

Livebook

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/

Ways to run Elixir

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

Language gotchas

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).

if expression

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

other expressions

The rules for if apply to cond, case, with as well. Avoid unless. You probably don't need receive.

cond

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

case

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

with

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

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]

Key conventions

Boolean functions

  • ...? is the convention of how to name a boolean function
  • is_ 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.

Information retrieval

  • functions named get return nil 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 like get 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.

Gotchas

keyword lists and maps

This is a syntactic transform.

in lists

[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]

in function calls

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)

in maps

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"}

Default parameters for functions

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

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}

boolean operators

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.

Anonymous function shortcut

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.

Calling anonymous functions

Anonymous functions are called using function_name.(args...)

Example:

my_fun = &(&1 + 10)
my_fun.(20) # --> 30

sigils

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.

Key sigils:

  • ~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

  • 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 work

this 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.

charlists vs strings

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".

Bracket Access

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.

Map matching

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: ....

Key functions

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

Key, Preferred Libraries

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment