Skip to content

Instantly share code, notes, and snippets.

@azer
Last active October 16, 2022 15:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save azer/2f93b4a8bbaa006dd2f47c503e234383 to your computer and use it in GitHub Desktop.
Save azer/2f93b4a8bbaa006dd2f47c503e234383 to your computer and use it in GitHub Desktop.
Notes on Elixir

Elixir

Table of Contents

Basic Types

iex> 1          # integer
iex> 0x1F       # integer
iex> 1.0        # float
iex> true       # boolean
iex> :atom      # atom / symbol
iex> "elixir"   # string
iex> [1, 2, 3]  # list
iex> {1, 2, 3}  # tuple

Docs

iex> h trunc/1

Anonymous functions

iex> add = fn a, b -> a + b end
#Function<12.71889879/2 in :erl_eval.expr/5>
iex> add.(1, 2)
3

Linked lists:

iex> [1, 2, true, 3]
[1, 2, true, 3]
iex> length [1, 2, 3]
3
iex> [1, 2, 3] ++ [4, 5, 6]
[1, 2, 3, 4, 5, 6]

When Elixir sees a list of printable ASCII numbers, Elixir will print that as a charlist:

iex> [104, 101, 108, 108, 111]
'hello'
iex> i 'hello'
Raw representation
  [104, 101, 108, 108, 111]

Tuples:

iex> {:ok, "hello"}
{:ok, "hello"}
iex> tuple_size {:ok, "hello"}
2
iex> put_elem(tuple, 2, :e)
{:a, :b, :e, :
iex> elem(tuple, 1)
"hello"

Operators

Arithmetic:

  • +
  • -
  • *
  • /

List manipulation:

  • ++
  • --

String concatenation:

  • <>

Boolean:

  • or
  • and
  • not
iex> 1 and true
** (BadBooleanError) expected a boolean on left-side of "and", got: 1
  • ||
  • &&
  • !
iex> 1 || true
1
iex> !nil
true

Comparison:

  • ==
  • !=
  • ===
  • <=
  • `>=
  • <
  • >

Comparing two different data types:

iex> 1 < :atom
true

Sorting order, lower to higher:

number, atom, reference, function, port, pid, tuple, map, list, bitstring

Pattern Matching

Match operator

iex> x = 1
1
iex> 1 = x
1
iex> 2 = x
** (MatchError) no match of right hand side value: 1

Pattern Matching

iex> {a, b, c} = {:hello, "world", 42}
{:hello, "world", 42}
iex> a
:hello
iex> b
"world"

Errors:

iex> {a, b, c} = {:hello, "world"}
** (MatchError) no match of right hand side value: {:hello, "world"}
iex> {a, b, c} = [:hello, "world", 42]
** (MatchError) no match of right hand side value: [:hello, "world", 42]

Lists:

iex> [a, b, c] = [1, 2, 3]
[1, 2, 3]

Head/tail

iex> [head | tail] = [1, 2, 3]
[1, 2, 3]
iex> head
1
iex> tail
[2, 3]

Pin operator

Use the pin operator ^ when you want to pattern match against a variable’s existing value rather than rebinding the variable.

iex> x = 1
1
iex> ^x = 2
** (MatchError) no match of right hand side value: 2

Pattern matching w/ pin operator:

iex> x = 1
1
iex> [^x, 2, 3] = [1, 2, 3]
[1, 2, 3]
iex> {y, ^x} = {2, 2}
** (MatchError) no match of right hand side value: {2, 2}

Underscore:

iex> [head | _] = [1, 2, 3]
[1, 2, 3]
iex> head
1

case, cond and if

case

iex> case {1, 2, 3} do
...>   {4, 5, 6} ->
...>     "This clause won't match"
...>   {1, x, 3} ->
...>     "This clause will match and bind x to 2 in this clause"
...>   _ ->
...>     "This clause would match any value"
...> end
"This clause will match and bind x to 2 in this clause"

cond

iex> cond do
...>   2 + 2 == 5 ->
...>     "This will not be true"
...>   2 * 2 == 3 ->
...>     "Nor this"
...>   1 + 1 == 2 ->
...>     "But this will"
...> end
"But this will"

if / unless

iex> if true do
...>   "This works!"
...> end
"This works!"
iex> unless true do
...>   "This will never be seen"
...> end
nil

Variable scoping:

iex> x = 1
1
iex> if true do
...>   x = x + 1
...> end
2
iex> x
1
iex> x = if true do
...>   x + 1
...> else
...>   x
...> end
iex > x
2

Binaries

iex> string = "hello"
"hello"
iex> is_binary(string)
true

You can use a ? in front of a character literal to reveal its code point:

iex> ?a
97
iex> string = "héllo"
"héllo"
iex> String.length(string)
5
iex> byte_size(string)
6
iex > String.length("👩‍🚒")
1
iex> String.codepoints("👩‍🚒")
["👩", "‍", "🚒"]
iex> String.graphemes("👩‍🚒")
["👩‍🚒"]

To see the exact bytes that a string would be stored in a file, concatenate the null byte <<0>> to it:

iex> "hełło" <> <<0>>
<<104, 101, 197, 130, 197, 130, 111, 0>>
iex> IO.inspect("hełło", binaries: :as_binaries)
<<104, 101, 197, 130, 197, 130, 111>>

Bitstrings

A bitstring is a fundamental data type in Elixir, denoted with the <<>> syntax. A bitstring is a contiguous sequence of bits in memory.

iex> <<42>> == <<42::8>>
true
iex> <<3::4>>
<<3::size(4)>>

By default, 8 bits (i.e. 1 byte) is used to store each number in a bitstring, but you can manually specify the number of bits via a ::n modifier to denote the size in n bits:

iex> <<42>> == <<42::8>>
true
iex> <<3::4>>
<<3::size(4)>>

For example, the decimal number 3 when represented with 4 bits in base 2 would be 0011, which is equivalent to the values 0, 0, 1, 1, each stored using 1 bit:

iex> <<0::1, 0::1, 1::1, 1::1>> == <<3::4>>
true

257 in base 2 would be represented as 100000001, but since we have reserved only 8 bits for its representation (by default), the left-most bit is ignored and the value becomes truncated to 00000001, or simply 1 in decimal.

iex> <<1>> == <<257>>
true

Binaries

iex> is_binary(<<3::4>>)
false
iex> is_bitstring(<<0, 255, 42>>)
true
iex> is_binary(<<0, 255, 42>>)
true
iex> is_binary(<<42::16>>)
true

Pattern matching:

iex> <<0, 1, x>> = <<0, 1, 2>>
<<0, 1, 2>>
iex> x
2
iex> <<0, 1, x::binary>> = <<0, 1, 2, 3>> # match binary of unknown size w/ `binary` modifier
<<0, 1, 2, 3>>

binary-size modifier:

iex> <<head::binary-size(2), rest::binary>> = <<0, 1, 2, 3>>
<<0, 1, 2, 3>>
iex> head
<<0, 1>>
iex> rest
<<2, 3>>

A string is a UTF-8 encoded binary, where the code point for each character is encoded using 1 to 4 bytes.

iex> is_binary("hello")
true

<> is actually a binary concatenation operator:

iex> "a" <> "ha"
"aha"
iex> <<0, 1>> <> <<2, 3>>
<<0, 1, 2, 3>>

Given that strings are binaries, we can also pattern match on strings:

iex> <<head, rest::binary>> = "banana"
"banana"
iex> head == ?b
true
iex> rest
"anana"

Charlists

A charlist is a list of integers where all the integers are valid code points. In practice, you will not come across them often, only in specific scenarios such as interfacing with older Erlang libraries that do not accept binaries as arguments.

iex> 'hello'
'hello'
iex> [?h, ?e, ?l, ?l, ?o]
'hello'

if you are storing a list of integers that happen to range between 0 and 127, by default IEx will interpret this as a charlist and it will display the corresponding ASCII characters.

iex> heartbeats_per_minute = [99, 97, 116]
'cat'

String (binary) concatenation uses the <> operator but charlists, being lists, use the list concatenation operator ++:

iex> 'this ' <> 'fails'
** (ArgumentError) expected binary argument in <> operator but got: 'this '
    (elixir) lib/kernel.ex:1821: Kernel.wrap_concatenation/3
    (elixir) lib/kernel.ex:1808: Kernel.extract_concatenations/2
    (elixir) expanding macro: Kernel.<>/2
    iex:1: (file)
iex> 'this ' ++ 'works'
'this works'
iex> "he" ++ "llo"
** (ArgumentError) argument error
    :erlang.++("he", "llo")
iex> "he" <> "llo"
"hello"

Keyword Lists and Maps

Keyword Lists

Keyword lists are a data-structure used to pass options to functions.

  • Keys must be atoms.
  • Keys are ordered, as specified by the developer.
  • Keys can be given more than once.
iex> String.split("1  2  3", " ")
["1", "", "2", "", "3"]
iex> String.split("1  2  3", " ", [trim: true])
["1", "2", "3"]
# when a keyword list is the last argument of a function, we can skip the brackets and write:
iex> String.split("1  2  3", " ", trim: true)
["1", "2", "3"]

Keyword lists are 2-item tuples where the first element (the key) is an atom and the second element can be any value. Both representations are the same:

iex> [{:trim, true}] == [trim: true]
true

We can use all operations available to lists:

iex> list ++ [c: 3]
[a: 1, b: 2, c: 3]
iex> list[:a]
1
iex> list[:b]
2

Values added to the front are the ones fetched:

iex> new_list = [a: 0] ++ list
[a: 0, a: 1, b: 2]
iex> new_list[:a]
0

Maps

Whenever you need a key-value store, maps are the “go to” data structure in Elixir.

  • Maps allow any value as a key.
  • Maps’ keys do not follow any ordering.
iex> map = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}
iex> map[:a]
1
iex> map[2]
:b
iex> map[:c]
nil

Pattern matching:

iex> %{:a => a} = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}
iex> a
1

Variables can be used when accessing, matching and adding map keys:

iex> n = 1
1
iex> map = %{n => :one}
%{1 => :one}
iex> map[n]
:one

Map module:

iex> Map.get(%{:a => 1, 2 => :b}, :a)
1
iex> Map.put(%{:a => 1, 2 => :b}, :c, 3)
%{2 => :b, :a => 1, :c => 3}
iex> Map.to_list(%{:a => 1, 2 => :b})
[{2, :b}, {:a, 1}]

Update syntax:

iex> map = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}

iex> %{map | 2 => "two"}
%{2 => "two", :a => 1}

When all the keys in a map are atoms, you can use the keyword syntax for convenience:

iex> map = %{a: 1, b: 2}
%{a: 1, b: 2}

Syntax for accessing atom keys:

iex> map = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}

iex> map.a
1
iex> map.c
** (KeyError) key :c not found in: %{2 => :b, :a => 1}

do-blocks

do blocks are nothing more than a syntax convenience on top of keywords.

iex> if true do
...>   "This will be seen"
...> else
...>   "This won't"
...> end
"This will be seen"

We can rewrite the above to:

iex> if true, do: "This will be seen", else: "This won't"
"This will be seen"

Nested Data Structures

iex> users = [
  john: %{name: "John", age: 27, languages: ["Erlang", "Ruby", "Elixir"]},
  mary: %{name: "Mary", age: 29, languages: ["Elixir", "F#", "Clojure"]}
]
iex> users[:john].age
27

Update value w/ put_in:

iex> users = put_in users[:john].age, 31
[
  john: %{age: 31, languages: ["Erlang", "Ruby", "Elixir"], name: "John"},
  mary: %{age: 29, languages: ["Elixir", "F#", "Clojure"], name: "Mary"}
]

The update_in/2 macro is similar but allows us to pass a function that controls how the value changes. For example, let’s remove “Clojure” from Mary’s list of languages:

iex> users = update_in users[:mary].languages, fn languages -> List.delete(languages, "Clojure") end
[
  john: %{age: 31, languages: ["Erlang", "Ruby", "Elixir"], name: "John"},
  mary: %{age: 29, languages: ["Elixir", "F#"], name: "Mary"}
]

Modules and functions

Elixir projects are usually organized into three directories:

  • _build - contains compilation artifacts
  • lib - contains Elixir code (usually .ex files)
  • test - contains tests (usually .exs files)
iex> defmodule Math do
...>   def sum(a, b) do
...>     a + b
...>   end
...> end

iex> Math.sum(1, 2)
3

Compile using elixirc to generate a BEAM file containing bytecode;

$ elixirc math.ex # will output Elixir.Math.beam

If we start iex again, our module definition will be available (iex is started in the same directory):

iex> Math.sum(1, 2)
3

Scripting

.ex files are meant to be compiled while .exs files are used for scripting.

defmodule Math do
  def sum(a, b) do
    a + b
  end
end

IO.puts Math.sum(1, 2)

And execute it as:

$ elixir math.exs

Named Functions

defmodule Math do
  def sum(a, b) do
    do_sum(a, b)
  end

  defp do_sum(a, b) do
    a + b
  end
end

IO.puts Math.sum(1, 2)    #=> 3
IO.puts Math.do_sum(1, 2) #=> ** (UndefinedFunctionError)

Function declarations also support guards and multiple clauses. If a function has several clauses, Elixir will try each clause until it finds one that matches.

defmodule Math do
  def zero?(0) do
    true
  end

  def zero?(x) when is_integer(x) do
    false
  end
end

IO.puts Math.zero?(0)         #=> true
IO.puts Math.zero?(1)         #=> false
IO.puts Math.zero?([1, 2, 3]) #=> ** (FunctionClauseError)
IO.puts Math.zero?(0.0)       #=> ** (FunctionClauseError)

We can edit it to look like this and it will provide the same behaviour:

defmodule Math do
  def zero?(0), do: true
  def zero?(x) when is_integer(x), do: false
end

Function Capturing

iex> Math.zero?(0)
true
iex> fun = &Math.zero?/1
&Math.zero?/1
iex> is_function(fun)
true
iex> fun.(0)
true

You can also capture operators:

iex> add = &+/2
&:erlang.+/2
iex> add.(1, 2)
3

The capture syntax can also be used as a shortcut for creating functions:

iex> fun = &(&1 + 1)
#Function<6.71889879/1 in :erl_eval.expr/5>
iex> fun.(1)
2

iex> fun2 = &"Good #{&1}"
#Function<6.127694169/1 in :erl_eval.expr/5>
iex> fun2.("morning")
"Good morning"

The &1 represents the first argument passed into the function. &(&1 + 1) above is exactly the same as fn x -> x + 1 end

Default Arguments

defmodule Concat do
  def join(a, b, sep \\ " ") do
    a <> sep <> b
  end
end

IO.puts Concat.join("Hello", "world")      #=> Hello world
IO.puts Concat.join("Hello", "world", "_") #=> Hello_world

If a function with default values has multiple clauses, it is required to create a function head (a function definition without a body) for declaring defaults:

defmodule Concat do
  # A function head declaring defaults
  def join(a, b \\ nil, sep \\ " ")

  def join(a, b, _sep) when is_nil(b) do
    a
  end

  def join(a, b, sep) do
    a <> sep <> b
  end
end

IO.puts Concat.join("Hello", "world")      #=> Hello world
IO.puts Concat.join("Hello", "world", "_") #=> Hello_world
IO.puts Concat.join("Hello")               #=> Hello

Recursion

Example:

defmodule Recursion do
  def print_multiple_times(msg, n) when n > 0 do
    IO.puts(msg)
    print_multiple_times(msg, n - 1)
  end

  def print_multiple_times(_msg, 0) do
    :ok
  end
end

Recursion.print_multiple_times("Hello!", 3)
# Hello!
# Hello!
# Hello!
:ok

Reduce and map:

defmodule Math do
  def sum_list([head | tail], accumulator) do
    sum_list(tail, head + accumulator)
  end

  def sum_list([], accumulator) do
    accumulator
  end
end

IO.puts Math.sum_list([1, 2, 3], 0) #=> 6

Double all of the values in a list:

defmodule Math do
  def double_each([head | tail]) do
    [head * 2 | double_each(tail)]
  end

  def double_each([]) do
    []
  end
end

The examples above could be written as:

iex> Enum.reduce([1, 2, 3], 0, fn(x, acc) -> x + acc end)
6
iex> Enum.map([1, 2, 3], fn(x) -> x * 2 end)
[2, 4, 6]

Or, using the capture syntax:

iex> Enum.reduce([1, 2, 3], 0, &+/2)
6
iex> Enum.map([1, 2, 3], &(&1 * 2))
[2, 4, 6]

Enumerables and Streams

All the functions in the Enum module are eager. Many functions expect an enumerable and return a list back:

iex> odd? = &(rem(&1, 2) != 0)
#Function<6.80484245/1 in :erl_eval.expr/5>
iex> Enum.filter(1..3, odd?)
[1, 3]

Pipe operator

iex> 1..100_000 |> Enum.map(&(&1 * 3)) |> Enum.filter(odd?) |> Enum.sum()
7500000000

Equivalent of

iex> Enum.sum(Enum.filter(Enum.map(1..100_000, &(&1 * 3)), odd?))
7500000000

Streams

As an alternative to Enum, Elixir provides the Stream module which supports lazy operations:

iex> 1..100_000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?) |> Enum.sum
7500000000

Instead of generating intermediate lists, streams build a series of computations that are invoked only when we pass the underlying stream to the Enum module. Streams are useful when working with large, possibly infinite, collections.

Stream.cycle/1 can be used to create a stream that cycles a given enumerable infinitely.

iex> stream = Stream.cycle([1, 2, 3])
#Function<15.16982430/2 in Stream.unfold/2>
iex> Enum.take(stream, 10)
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1]

Stream.unfold/2 can be used to generate values from a given initial value:

iex> stream = Stream.unfold("hełło", &String.next_codepoint/1)
#Function<39.75994740/2 in Stream.unfold/2>
iex> Enum.take(stream, 3)
["h", "e", "ł"]

Fetch the first 10 lines of the file you have selected;

iex> stream = File.stream!("path/to/file")
%File.Stream{
  line_or_bytes: :line,
  modes: [:raw, :read_ahead, :binary],
  path: "path/to/file",
  raw: true
}
iex> Enum.take(stream, 10)

Processes

sAll code runs inside processes. Processes are isolated from each other, run concurrent to one another and communicate via message passing. Processes are not only the basis for concurrency in Elixir, but they also provide the means for building distributed and fault-tolerant programs.

Elixir’s processes should not be confused with operating system processes. Processes in Elixir are extremely lightweight in terms of memory and CPU (even compared to threads as used in many other programming languages). Because of this, it is not uncommon to have tens or even hundreds of thousands of processes running simultaneously.

spawn

spawn/1 takes a function which it will execute in another process.

iex> pid = spawn(fn -> 1 + 2 end)
#PID<0.43.0>
iex> Process.alive?(pid)
false

Retrieve the PID of the current process by calling self/0:

iex> self()
#PID<0.41.0>
iex> Process.alive?(self())
true

send and receive

iex> send(self(), {:hello, "world"})
{:hello, "world"}
iex> receive do
...>   {:hello, msg} -> msg
...>   {:world, _msg} -> "won't match"
...> end
"world"

Timeout:

iex> receive do
...>   {:hello, msg}  -> msg
...> after
...>   1_000 -> "nothing after 1s"
...> end
"nothing after 1s"

Links

If we want the failure in one process to propagate to another one, we should link them. This can be done with spawn_link/1:

iex> self()
#PID<0.41.0>
iex> spawn_link(fn -> raise "oops" end)

** (EXIT from #PID<0.41.0>) evaluator process exited with reason: an exception was raised:
    ** (RuntimeError) oops
        (stdlib) erl_eval.erl:668: :erl_eval.do_apply/6

[error] Process #PID<0.289.0> raised an exception
** (RuntimeError) oops
    (stdlib) erl_eval.erl:668: :erl_eval.do_apply/6

Tasks

Tasks build on top of the spawn functions to provide better error reports and introspection:

iex> Task.start(fn -> raise "oops" end)
{:ok, #PID<0.55.0>}

15:22:33.046 [error] Task #PID<0.55.0> started from #PID<0.53.0> terminating
** (RuntimeError) oops
    (stdlib) erl_eval.erl:668: :erl_eval.do_apply/6
    (elixir) lib/task/supervised.ex:85: Task.Supervised.do_apply/2
    (stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3
Function: #Function<20.99386804/0 in :erl_eval.expr/5>
    Args: []

State

We can write processes that loop infinitely, maintain state, and send and receive messages. As an example, let’s write a module that starts new processes that work as a key-value store:

# kv.exs
defmodule KV do
  def start_link do
    Task.start_link(fn -> loop(%{}) end)
  end

  defp loop(map) do
    receive do
      {:get, key, caller} ->
        send caller, Map.get(map, key)
        loop(map)
      {:put, key, value} ->
        loop(Map.put(map, key, value))
    end
  end
end
iex> {:ok, pid} = KV.start_link()
{:ok, #PID<0.62.0>}
iex> send(pid, {:get, :hello, self()})
{:get, :hello, #PID<0.41.0>}
iex> flush()
nil
:ok
iex> send(pid, {:put, :hello, :world})
{:put, :hello, :world}
iex> send(pid, {:get, :hello, self()})
{:get, :hello, #PID<0.41.0>}
iex> flush()
:world
:ok

Register processes with name:

iex> Process.register(pid, :kv)
true
iex> send(:kv, {:get, :hello, self()})
{:get, :hello, #PID<0.41.0>}
iex> flush()
:world
:ok

Maintaining state using Agents:

iex> {:ok, pid} = Agent.start_link(fn -> %{} end)
{:ok, #PID<0.72.0>}
iex> Agent.update(pid, fn map -> Map.put(map, :hello, :world) end)
:ok
iex> Agent.get(pid, fn map -> Map.get(map, :hello) end)
:world

IO and the file system

iex> IO.puts("hello world")
hello world
:ok
iex> IO.gets("yes or no? ")
yes or no? yes
"yes\n"
iex> iex> IO.puts(:stderr, "hello world")
hello world
:ok

The File module

By default, files are opened in binary mode:

iex> {:ok, file} = File.open("path/to/file/hello", [:write])
{:ok, #PID<0.47.0>}
iex> IO.binwrite(file, "world")
:ok
iex> File.close(file)
:ok
iex> File.read("path/to/file/hello")
{:ok, "world"}

Some other functions under File:

  • rm
  • mkdir
  • mkdir_p
  • cp
  • cp_r
  • rm_rf

Handling errors:

iex> File.read!("path/to/file/hello")
"world"
iex> File.read("path/to/file/unknown")
{:error, :enoent}
iex> File.read!("path/to/file/unknown")
** (File.Error) could not read file "path/to/file/unknown": no such file or directory

The Path Module

iex> Path.join("foo", "bar")
"foo/bar"
iex> Path.expand("~/hello")
"/Users/jose/hello"

Processes

iex> {:ok, file} = File.open("hello", [:write])
{:ok, #PID<0.47.0>}

Given a file is a process, when you write to a file that has been closed, you are actually sending a message to a process which has been terminated:

iex> File.close(file)
:ok
iex> IO.write(file, "is anybody out there")
{:error, :terminated}

iodata and chardata

Most of the IO functions in Elixir also accept either “iodata” or “chardata”.

Copying can be quite expensive for large strings. Elixir allow you to pass list of strings:

name = "Mary"
IO.puts(["Hello ", name, "!"])

Imagine you have a list of values, such as ["apple", "banana", "lemon"] that you want to write to disk separated by commas. How can you achieve this?

One option is to use Enum.join/2 and convert the values to a string:

iex> Enum.join(["apple", "banana", "lemon"], ",")
"apple,banana,lemon"

However, we can pass a list of strings to the IO/File functions. So instead we can do:

iex> Enum.intersperse(["apple", "banana", "lemon"], ",")
["apple", ",", "banana", ",", "lemon"]

“iodata” and “chardata” do not only contain strings, but they may contain arbitrary nested lists of strings too:

iex> IO.puts(["apple", [",", "banana", [",", "lemon"]]])
apple,banana,lemon
:ok

Print comma separated list of values by using ?, as separator, which is the integer representing a comma (44):

iex(19)> IO.puts(["apple", ?,, "banana", ?,, "lemon"])
apple,banana,lemon
:ok
  • For iodata, the integers represent bytes.
  • For chardata, the integers represent Unicode codepoints.
  • For ASCII characters, the byte representation is the same as the codepoint representation, so it fits both classifications.

The default IO device works with chardata, which means we can do:

iex> IO.puts([?O, ?l, , ?\s, "Mary", ?!])
Olá Mary!
:ok

charlist construct:

iex> ~c"hello"
~c"hello"

summary:

  • iodata and chardata are lists of binaries and integers. Those binaries and integers can be arbitrarily nested inside lists. Their goal is to give flexibility and performance when working with IO devices and files
  • the choice between iodata and chardata depends on the encoding of the IO device. If the file is opened without encoding, the file expects iodata, and the functions in the IO module starting with bin* must be used. The default IO device (:stdio) and files opened with :utf8 encoding work expect chardata and work with the remaining functions in the IO module
  • charlists are a special case of chardata, where it exclusively uses a list of integers Unicode codepoints. They can be created with the ~c sigil. Lists of integers are automatically printed using the ~c sigil if all integers in a list represent printable ASCII codepoints.

alias, require, and import

alias, require, import

# Alias the module so it can be called as Bar instead of Foo.Bar
alias Foo.Bar, as: Bar

# Require the module in order to use its macros
require Foo

# Import functions from Foo so they can be called without the `Foo.` prefix
import Foo

# Invokes the custom code defined in Foo as an extension point
use Foo

Alias

alias Math.List

Is the same as:

alias Math.List, as: List

Require

Public functions in modules are globally available, but in order to use macros, you need to opt-in by requiring the module they are defined in.

iex> Integer.is_odd(3)
** (UndefinedFunctionError) function Integer.is_odd/1 is undefined or private. However, there is a macro with the same name and arity. Be sure to require Integer if you intend to invoke this macro
    (elixir) Integer.is_odd(3)
iex> require Integer
Integer
iex> Integer.is_odd(3)
true

Import

To use the duplicate/2 function from the List module several times, we can import it:

iex> import List, only: [duplicate: 2]
List
iex> duplicate(:ok, 3)
[:ok, :ok, :ok]

We can import specific macros or functions inside function definitions:

defmodule Math do
  def some_function do
    import List, only: [duplicate: 2]
    duplicate(:ok, 10)
  end
end

Note that imports are generally discouraged in the language. When working on your own code, prefer alias to import.

Use

The use macro is frequently used as an extension point. This means that, when you use a module FooBar, you allow that module to inject any code in the current module, such as importing itself or other modules, defining new functions, setting a module state, etc.

defmodule AssertionTest do
  use ExUnit.Case, async: true

  test "always pass" do
    assert true
  end
end

Behind the scenes, use requires the given module and then calls the using/1 callback on it allowing the module to inject some code into the current context.

Understanding Aliases

An alias in Elixir is a capitalized identifier (like String, Keyword, etc) which is converted to an atom during compilation.

iex> is_atom(String)
true
iex> to_string(String)
"Elixir.String"
iex> :"Elixir.String" == String
true
iex> List.flatten([1, [2], 3])
[1, 2, 3]
iex> :"Elixir.List".flatten([1, [2], 3])
[1, 2, 3]

That’s the mechanism we use to call Erlang modules:

iex> :lists.flatten([1, [2], 3])
[1, 2, 3]

Module Nesting

defmodule Foo do
  defmodule Bar do
  
  end
end

The example above will define two modules: Foo and Foo.Bar. The second can be accessed as Bar inside Foo as long as they are in the same lexical scope.

The above could also be written as:

defmodule Foo.Bar do
end

defmodule Foo do
  alias Foo.Bar
  # Can still access it as `Bar`
end

Alias multiple modules at once:

alias MyApp.{Foo, Bar, Baz}

Module Attributes

Module attributes in Elixir serve three purposes:

  • They serve to annotate the module, often with information to be used by the user or the VM.
  • They work as constants.
  • They work as a temporary module storage to be used during compilation.

Annotations

  • @moduledoc - provides documentation for the current module.
  • @doc - provides documentation for the function or macro that follows the attribute.
  • @spec - provides a typespec for the function that follows the attribute.
  • @behaviour - (notice the British spelling) used for specifying an OTP or user-defined behaviour.
defmodule MyServer do
  @moduledoc "My server code."
end
defmodule Math do
  @moduledoc """
  Provides math-related functions.

  ## Examples

      iex> Math.sum(1, 2)
      3

  """

  @doc """
  Calculates the sum of two numbers.
  """
  def sum(a, b), do: a + b
end

Constants

defmodule MyServer do
  @initial_state %{host: "127.0.0.1", port: 3456}
  IO.inspect @initial_state
end

Functions can be called:

defmodule MyApp.Status do
  @service URI.parse("https://example.com")
  def status(email) do
    SomeHttpClient.get(@service)
  end
end

The function above will be called at compilation time and its return value, not the function call itself, is what will be substituted in for the attribute. So the above will effectively compile to this:

defmodule MyApp.Status do
  def status(email) do
    SomeHttpClient.get(%URI{
      authority: "example.com",
      host: "example.com",
      port: 443,
      scheme: "https"
    })
  end
end

Every time an attribute is read inside a function, Elixir takes a snapshot of its current value. Therefore if you read the same attribute multiple times inside multiple functions, you may end-up making multiple copies of it.

Instead of this:

def some_function, do: do_something_with(@example)
def another_function, do: do_something_else_with(@example)

Prefer this:

def some_function, do: do_something_with(example())
def another_function, do: do_something_else_with(example())
defp example, do: @example

Accumulating Attributes

Normally, repeating a module attribute will cause its value to be reassigned, but there are circumstances where you may want to configure the module attribute so that its values are accumulated:

defmodule Foo do
  Module.register_attribute __MODULE__, :param, accumulate: true

  @param :foo
  @param :bar
  # here @param == [:bar, :foo]
end
defmodule MyTest do
  use ExUnit.Case, async: true

  @tag :external
  @tag os: :unix
  test "contacts external service" do
    # ...
  end
end

Structs

Structs are extensions built on top of maps that provide compile-time checks and default values.

To define a struct, the defstruct construct is used:

iex> defmodule User do
...>   defstruct name: "John", age: 27
...> end
iex> %User{}
%User{age: 27, name: "John"}
iex> %User{name: "Jane"}
%User{age: 27, name: "Jane"}
iex> %User{oops: :field}
** (KeyError) key :oops not found in: %User{age: 27, name: "John"}

Accessing and updating structs

iex> john = %User{}
iex> john.name
"John"
iex> jane = %{john | name: "Jane"}
%User{age: 27, name: "Jane"}

Pattern matching

iex> %User{name: name} = john
%User{age: 27, name: "John"}
iex> name
"John"

Underneath they're bare maps

iex> is_map(john)
true
iex> john.__struct__
User
iex> jane = Map.put(%User{}, :name, "Jane")
%User{age: 27, name: "Jane"}
iex> Map.merge(jane, %User{name: "John"})
%User{age: 27, name: "John"}
iex> Map.keys(jane)
[:__struct__, :age, :name]

Default Values and required keys

If you don’t specify a default key value when defining a struct, nil will be assumed:

iex> defmodule User do
...>   defstruct [:email, name: "John", age: 27]
...> end
iex> %User{}
%User{age: 27, email: nil, name: "John"}

Enforce certain keys to be specified:

iex> defmodule Car do
...>   @enforce_keys [:make]
...>   defstruct [:model, :make]
...> end
iex> %Car{}
** (ArgumentError) the following keys must also be given when building struct Car: [:make]
    expanding struct: Car.__struct__/1

Protocols

Consider a simple utility module that would tell us the type of input variable:

defmodule Utility do
  def type(value) when is_binary(value), do: "string"
  def type(value) when is_integer(value), do: "integer"
  # ... other implementations ...
end

If the use of this module were confined to your own project, you would be able to keep defining new type/1 functions for each new data type. However, this code could be problematic if it was shared as a dependency by multiple apps because there would be no easy way to extend its functionality.

This is where protocols can help us: protocols allow us to extend the original behavior for as many data types as we need. That’s because dispatching on a protocol is available to any data type that has implemented the protocol and a protocol can be implemented by anyone, at any time.

defprotocol Utility do
  @spec type(t) :: String.t()
  def type(value)
end

defimpl Utility, for: BitString do
  def type(_value), do: "string"
end

defimpl Utility, for: Integer do
  def type(_value), do: "integer"
end
iex> Utility.type("foo")
"string"
iex> Utility.type(123)
"integer"

Example

We have two idioms for checking how many items there are in a data structure: length and size.

  • length means the information must be computed. For example, length(list) needs to traverse the whole list to calculate its length.
  • tuple_size(tuple) and byte_size(binary) do not depend on the tuple and binary size as the size information is pre-computed in the data structure.

We could implement a generic Size protocol that all data structures for which size is pre-computed would implement:

defprotocol Size do
  @doc "Calculates the size (and not the length!) of a data structure"
  def size(data)
end
defimpl Size, for: BitString do
  def size(string), do: byte_size(string)
end

defimpl Size, for: Map do
  def size(map), do: map_size(map)
end

defimpl Size, for: Tuple do
  def size(tuple), do: tuple_size(tuple)
end

Usage:

iex> Size.size("foo")
3
iex> Size.size({:ok, "hello"})
2
iex> Size.size(%{label: "some label"})
1

Passing a data type that doesn’t implement the protocol raises an error:

iex> Size.size([1, 2, 3])
** (Protocol.UndefinedError) protocol Size not implemented for [1, 2, 3]

It’s possible to implement protocols for all Elixir data types:

  • Atom
  • BitString
  • Float
  • Function
  • Integer
  • List
  • Map
  • PID
  • Port
  • Reference
  • Tuple

If desired, you could come up with your own semantics for the size of your struct.

defmodule User do
  defstruct [:name, :age]
end

defimpl Size, for: User do
  def size(_user), do: 2
end

Deriving

Elixir allows us to derive a protocol implementation based on the Any implementation. Let’s first implement Any as follows:

defimpl Size, for: Any do
  def size(_), do: 0
end

However, should we be fine with the implementation for Any, in order to use such implementation we would need to tell our struct to explicitly derive the Size protocol:

defmodule OtherUser do
  @derive [Size]
  defstruct [:name, :age]
end

Built-in protocols

iex> to_string :hello
"hello"

Notice that string interpolation in Elixir calls the to_string function:

iex> "age: #{25}"
"age: 25"

The snippet above only works because numbers implement the String.Chars protocol. Passing a tuple, for example, will lead to an error:

iex> tuple = {1, 2, 3}
{1, 2, 3}
iex> "tuple: #{tuple}"
** (Protocol.UndefinedError) protocol String.Chars not implemented for {1, 2, 3}

When there is a need to “print” a more complex data structure, one can use the inspect function, based on the Inspect protocol:

iex> "tuple: #{inspect tuple}"
"tuple: {1, 2, 3}"

The Inspect protocol is the protocol used to transform any data structure into a readable textual representation.

iex> {1, 2, 3}
{1, 2, 3}
iex> %User{}
%User{name: "john", age: 27}
iex> inspect &(&1+2)
"#Function<6.71889879/1 in :erl_eval.expr/5>"

Comprehensions

A comprehension is made of three parts: generators, filters, and collectables.

iex> for n <- 1..4, do: n * n
[1, 4, 9, 16]

pattern matching

iex> values = [good: 1, good: 2, bad: 3, good: 4]
iex> for {:good, n} <- values, do: n * n
[1, 4, 16]

filtering:

iex> for n <- 0..5, rem(n, 3) == 0, do: n * n
[0, 9]

Bitstring generators

iex> pixels = <<213, 45, 132, 64, 76, 32, 76, 0, 0, 234, 32, 15>>
iex> for <<r::8, g::8, b::8 <- pixels>>, do: {r, g, b}
[{213, 45, 132}, {64, 76, 32}, {76, 0, 0}, {234, 32, 15}]

Transforming values in a map:

iex> for {key, val} <- %{"a" => 1, "b" => 2}, into: %{}, do: {key, val * val}
%{"a" => 1, "b" => 4}

Sigils

Regular expressions:

# A regular expression that matches strings which contain "foo" or "bar":
iex> regex = ~r/foo|bar/
~r/foo|bar/
iex> "foo" =~ regex
true
iex> "bat" =~ regex
false

Strings:

iex> ~s(this is a string with "double" quotes, not 'single' ones)
"this is a string with \"double\" quotes, not 'single' ones"

Char lists:

iex> ~c(this is a char list containing 'single quotes')
'this is a char list containing \'single quotes\''

Word lists:

iex> ~w(foo bar bat)
["foo", "bar", "bat"]
iex> ~w(foo bar bat)a
[:foo, :bar, :bat]

Interpolation and escaping:

iex> ~s(String with escape codes \x26 #{"inter" <> "polation"})
"String with escape codes & interpolation"
iex> ~S(String without escape codes \x26 without #{interpolation})
"String without escape codes \\x26 without \#{interpolation}"

The following escape codes can be used in strings and char lists:

  • \ – single backslash
  • \a – bell/alert
  • \b – backspace
  • \d - delete
  • \e - escape
  • \f - form feed
  • \n – newline
  • \r – carriage return
  • \s – space
  • \t – tab
  • \v – vertical tab
  • \0 - null byte
  • \xDD - represents a single byte in hexadecimal (such as \x13)
  • \uDDDD and \u{D...} - represents a Unicode codepoint in hexadecimal (such as \u{1F600})

Calendar sigils

%Date{}

iex> d = ~D[2019-10-31]
~D[2019-10-31]
iex> d.day
31

%Time{}

iex> t = ~T[23:00:07.0]
~T[23:00:07.0]
iex> t.second
7

%NaiveDateTime{}

iex> ndt = ~N[2019-10-31 23:00:07]
~N[2019-10-31 23:00:07]

%DateTime{}

iex> dt = ~U[2019-10-31 19:59:03Z]
~U[2019-10-31 19:59:03Z]
iex> %DateTime{minute: minute, time_zone: time_zone} = dt
~U[2019-10-31 19:59:03Z]
iex> minute
59
iex> time_zone
"Etc/UTC"

Custom Sigils

iex> sigil_r(<<"foo">>, 'i')
~r"foo"i
iex> defmodule MySigils do
...>   def sigil_i(string, []), do: String.to_integer(string)
...>   def sigil_i(string, [?n]), do: -String.to_integer(string)
...> end
iex> import MySigils
iex> ~i(13)
13
iex> ~i(42)n
-42

try, catch, and rescue

iex> raise "oops"
** (RuntimeError) oops
iex> raise ArgumentError, message: "invalid argument foo"
** (ArgumentError) invalid argument foo

Custom errors

iex> defmodule MyError do
iex>   defexception message: "default message"
iex> end
iex> raise MyError
** (MyError) default message
iex> raise MyError, message: "custom message"
** (MyError) custom message

Rescue

iex> try do
...>   raise "oops"
...> rescue
...>   e in RuntimeError -> e
...> end
%RuntimeError{message: "oops"}

Elixir developers rarely use the try/rescue construct. You can use pattern matching using the case construct:

iex> case File.read("hello") do
...>   {:ok, body} -> IO.puts("Success: #{body}")
...>   {:error, reason} -> IO.puts("Error: #{reason}")
...> end

The convention is to;

  • create a function (foo) which returns {:ok, result} or {:error, reason} tuples
  • and another function (foo!, same name but with a trailing !) that takes the same arguments as foo but which raises an exception if there’s an error.

Fail fast / Let it crash

One saying that is common in the Erlang community, as well as Elixir’s, is “fail fast” / “let it crash”. The idea behind let it crash is that, in case something unexpected happens, it is best to let the exception happen, without rescuing it.

Reraise

try do
  ... some code ...
rescue
  e ->
    Logger.error(Exception.format(:error, e, __STACKTRACE__))
    reraise e, __STACKTRACE__
end

Throw/catch

iex> try do
...>   Enum.each(-50..50, fn x ->
...>     if rem(x, 13) == 0, do: throw(x)
...>   end)
...>   "Got nothing"
...> catch
...>   x -> "Got #{x}"
...> end
"Got -39"

Exits

When a process dies of “natural causes” (e.g., unhandled exceptions), it sends an exit signal.

iex> spawn_link(fn -> exit(1) end)
** (EXIT from #PID<0.56.0>) evaluator process exited with reason: 1
iex> try do
...>   exit("I am exiting")
...> catch
...>   :exit, _ -> "not really"
...> end
"not really"

After

The try/after construct allows you to clean up after some action that could potentially raise an error. For example, we can open a file and use an after clause to close it–even if something goes wrong:

iex> {:ok, file} = File.open("sample", [:utf8, :write])
iex> try do
...>   IO.write(file, "olá")
...>   raise "oops, something went wrong"
...> after
...>   File.close(file)
...> end
** (RuntimeError) oops, something went wrong

Elixir allows you to omit the try line:

iex> defmodule RunAfter do
...>   def without_even_trying do
...>     raise "oops"
...>   after
...>     IO.puts "cleaning up!"
...>   end
...> end
iex> RunAfter.without_even_trying
cleaning up!
** (RuntimeError) oops

Else

iex> x = 2
2
iex> try do
...>   1 / x
...> rescue
...>   ArithmeticError ->
...>     :infinity
...> else
...>   y when y < 1 and y > -1 ->
...>     :small
...>   _ ->
...>     :large
...> end
:small

Optional Syntax

Elixir syntax allows developers to omit delimiters in a few occasions to make code more readable. For example, we learned that parentheses are optional:

iex> length([1, 2, 3]) == length [1, 2, 3]
true

and that do-end blocks are equivalent to keyword lists:

# do-end blocks
iex> if true do
...>   :this
...> else
...>   :that
...> end
:this

# keyword lists
iex> if true, do: :this, else: :that
:this

Walk-through

Example:

if variable? do
  Call.this()
else
  Call.that()
end
  1. do-end blocks are equivalent to keywords:
if variable?, do: Call.this(), else: Call.that()
  1. Keyword lists as last argument do not require square brackets, but let’s add them:
if variable?, [do: Call.this(), else: Call.that()]
  1. Keyword lists are the same as lists of two-element tuples:
if variable?, [{:do, Call.this()}, {:else, Call.that()}]

Finally, parentheses are optional, but let’s add them:

if(variable?, [{:do, Call.this()}, {:else, Call.that()}])

Another example:

defmodule Math do
  def add(a, b) do
    a + b
  end
end
defmodule(Math, [
  {:do, def(add(a, b), [{:do, a + b}])}
])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment