Skip to content

Instantly share code, notes, and snippets.

@fschuindt
Last active July 8, 2025 17:30
Show Gist options
  • Select an option

  • Save fschuindt/276043effe18b7649cf7d9cc8be1a14b to your computer and use it in GitHub Desktop.

Select an option

Save fschuindt/276043effe18b7649cf7d9cc8be1a14b to your computer and use it in GitHub Desktop.
My personal study notes on Elixir. It's basically a resume of http://elixir-lang.org/getting-started/ (Almost everything is copied). Still working on it!

Elixir Study Notes

Important Links

Notes

  • Get some help:
iex> i 'hello'
Term
  'hello'
Data type
  List
Description
  ...
Raw representation
  [104, 101, 108, 108, 111]
Reference modules
  List
  • When “counting” the number of elements in a data structure, Elixir also abides by a simple rule: the function is named size if the operation is in constant time (i.e. the value is pre-calculated) or length if the operation is linear (i.e. calculating the length gets slower as the input grows).
  • String concatenation is done with: <>.
  • Operators or, and, not can only accept boolean values. Besides these boolean operators, Elixir also provides ||, && and ! which accept arguments of any type. For these operators, all values except false and nil will evaluate to true.
  • The variable _ is special in that it can never be read from. Trying to read from it gives an unbound variable error.
  • Guard Clauses are neat:
# Anonymous functions can have guard clauses:
# They also apply to the 'case' statement, 'when'.
iex> f = fn
...>   x, y when x > 0 -> x + y
...>   x, y -> x * y
...> end
#Function<12.71889879/2 in :erl_eval.expr/5>
iex> f.(1, 3)
4
iex> f.(-1, 3)
-3

1 - Basic Types

Booleans

All good, just true and false. Nothing special.

Atoms

Pretty much like Lisp's Atoms, A.K.A. Symbols in Ruby.

Anonymous Functions

Functions delimited between fn and end.

# first class citizens
iex> add = fn a, b -> a + b end
iex> add.(3, 2)

Lists

Describes itself.

# Add or subtract using ++ or --
iex> [2, 23, 42, 11, true]
iex> list = [1, 2, 3]
# Get head and tail.
iex> hd(list)
1
iex>tl(list)
[2, 3]
  • When Elixir sees a list of printable ASCII numbers, Elixir will print that as a char list (literally a list of characters).
  • Single-quotes are char lists, double-quotes are strings.

Tuples

Similar to lists, but stored in memory, all data is availible with no recursion needed.

iex> {:ok, "hello"}
{:ok, "hello"}
iex> tuple_size {:ok, "hello"}
2

List or Tuples?

Accessing the length of a list is a linear operation: we need to traverse the whole list in order to figure out its size. Updating a list is fast as long as we are prepending elements.

Tuples, on the other hand, are stored contiguously in memory. This means getting the tuple size or accessing an element by index is fast. However, updating or adding elements to tuples is expensive because it requires copying the whole tuple in memory.

When “counting” the number of elements in a data structure, Elixir also abides by a simple rule: the function is named size if the operation is in constant time (i.e. the value is pre-calculated) or length if the operation is linear (i.e. calculating the length gets slower as the input grows).

4 - Pattern Matching

The match operator is not only used to match against simple values, but it is also useful for destructuring more complex data types. For example, we can pattern match on tuples:

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

A list also supports matching on its own head and tail:

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

The pin operator ^ should be used when you want to pattern match against an existing variable’s value rather than rebinding the variable.

5 - case, cond and if

case

Behavious pretty much as the classic case statement.

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

If you want to pattern match against an existing variable, you need to use the ^ operator:

iex> x = 1
1
iex> case 10 do
...>   ^x -> "Won't match"
...>   _  -> "Will match"
...> end

Another cool example, now with clauses conditions:

iex> case {1, 2, 3} do
...>   {1, x, 3} when x > 0 ->
...>     "Will match"
...>   _ ->
...>     "Would match, if guard condition were not satisfied"
...> end
  • If none of the clauses match, an error is raised.

cond

case is useful when you need to match against different values. However, in many circumstances, we want to check different conditions and find the first one that evaluates to true. In such cases, one may use cond.

iex> cond do
...>   2 + 2 == 5 ->
...>     "This will not be true"
...>   2 * 2 == 3 ->
...>     "Nor this"
...>   1 + 1 == 2 ->
...>     "But this will"
...> end
  • This is equivalent to else and if clauses in many imperative languages.
  • If none of the conditions return true, an error is raised. For this reason, it may be necessary to add a final condition, equal to true, which will always match.

if and unless

Are useful when you need to check for just one condition, also pro provides a else statement.

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

do / end blocks

Equivalent to { / }, it's also possible things like:

iex> if false, do: :this, else: :that
# Expressions like:
iex> is_number if true do
...>  1 + 2
...> end
** (CompileError) undefined function: is_number/2
# Should be:
iex> is_number(if true do
...>  1 + 2
...> end)
true

6 - Binaries, strings and char lists

Binaries and bitstrings

You can define a binary using <<>>. It's just a sequence of bytes.

  • The string concatenation operation is actually a binary concatenation operator <>.
  • A binary is a bitstring where the number of bits is divisible by 8. Smaller bit are just bitstrings!
  • A string is a UTF-8 encoded binary, and a binary is a bitstring where the number of bits is divisible by 8.

A common trick in Elixir is to concatenate the null byte <<0>> to a string to see its inner binary representation:

iex> "hełło" <> <<0>>
<<104, 101, 197, 130, 197, 130, 111, 0>>

Char lists

A char list is nothing more than a list of characters.
Char list contains the code points of the characters between single-quotes (note that IEx will only output code points if any of the chars is outside the ASCII range). So while double-quotes represent a string (i.e. a binary), single-quotes represents a char list (i.e. a list).

7 - Keywords and maps

Keyword list

It's a associative data structure. In Elixir, when we have a list of tuples and the first item of the tuple (i.e. the key) is an atom, we call it a keyword list:

iex> list = [{:a, 1}, {:b, 2}]
[a: 1, b: 2]
iex> list == [a: 1, b: 2]
true
iex> list[:a]
1
  • It's the default mechanism for passing options to functions in Elixir.
  • Only allows Atoms as keys.
  • Ordered as specified by the developer.
  • Remember, though, keyword lists are simply lists, and as such they provide the same linear performance characteristics as lists. The longer the list, the longer it takes to read from. For bigger data use maps instead.

Maps

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

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

Interacts great with pattern matching:

iex> %{} = %{:a => 1, 2 => :b}
%{:a => 1, 2 => :b}
iex> %{:a => a} = %{:a => 1, 2 => :b}
%{:a => 1, 2 => :b}
iex> a
1
iex> %{:c => c} = %{:a => 1, 2 => :b}
** (MatchError) no match of right hand side value: %{2 => :b, :a => 1}

Better syntax when all keys are atoms:

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

Another interesting property of maps is that they provide their own syntax for updating and accessing atom keys:

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

Nested data structures (put_in/2 and update_in/2)

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

We have a keyword list of users where each value is a map containing the name, age and a list of programming languages each user likes. If we wanted to access the age for john, we could write:

iex> users[:john].age
27

It happens we can also use this same syntax for updating the value:

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, &List.delete(&1, "Clojure")
[john: %{age: 31, languages: ["Erlang", "Ruby", "Elixir"], name: "John"},
mary: %{age: 29, languages: ["Elixir", "F#"], name: "Mary"}]

There is more to learn about put_in/2 and update_in/2, including the get_and_update_in/2 that allows us to extract a value and update the data structure at once. There are also put_in/3, update_in/3 and get_and_update_in/3 which allow dynamic access into the data structure. Check their respective documentation in the Kernel module for more information.

8 - Modules

In Elixir we group several functions into modules.

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

iex> Math.sum(1, 2)
3

Compilation

Given a file math.ex:

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

This file can be compiled using elixirc:

$ elixirc math.ex

This will generate a file named Elixir.Math.beam containing the bytecode for the defined module. If we start iex again, our module definition will be available (provided that iex is started in the same directory the bytecode file is in).

Elixir projects are usually organized into three directories:

  • ebin - contains the compiled bytecode
  • lib - contains elixir code (usually .ex files)
  • test - contains tests (usually .exs files)

When working on actual projects, the build tool called mix will be responsible for compiling and setting up the proper paths for you.

Scripted mode

  • .ex - files to be compiled
  • .exs - files to run in scripted mode (Learning purposes)

Executing:

$ elixir math.exs

Named functions

  • def/2 - defines a function
  • defp/2 - defines a private function

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)

Similar to constructs like if, named functions support both do: and do/end block syntax, as we learned do/end is just a convenient syntax for the keyword list format. For example, we can edit math.exs to look like this:

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

Function capturing

Can actually be used to retrieve a named function as a function type. (Given the file)

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

Local or imported functions, like is_function/1, can be captured without the module:

iex> &is_function/1
&:erlang.is_function/1
iex> (&is_function/1).(fun)
true

Note 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

The &1 represents the first argument passed into the function.
The above is the same as fn x -> x + 1 end. It's useful for short function definitions.

If you want to capture a function from a module, you can do &Module.function():

iex> fun = &List.flatten(&1, &2)
&List.flatten/2
iex> fun.([1, [[2], 3]], [4, 5])
[1, 2, 3, 4, 5]

&List.flatten(&1, &2) is the same as writing fn(list, tail) -> List.flatten(list, tail) end which in this case is equivalent to &List.flatten/2. You can read more about the capture operator & in the Kernel.SpecialForms documentation.

Default arguments

Named functions 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 (without an actual body) for declaring defaults:

defmodule Concat do
  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
  • Default values won’t be evaluated during the function definition
  • When using default values, one must be careful to avoid overlapping function definitions

9 - Recursion

Loops through recursion

Beautifully without mutating:

defmodule Recursion do
  def print_multiple_times(msg, n) when n <= 1 do
    IO.puts msg
  end

  def print_multiple_times(msg, n) do
    IO.puts msg
    print_multiple_times(msg, n - 1)
  end
end

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

Reduce and map algorithms

Let’s now see how we can use the power of recursion to sum a list of numbers:

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

The process of taking a list and reducing it down to one value is known as a reduce algorithm and is central to functional programming.

What if we instead want to double all of the values in our list?

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

  def double_each([]) do
    []
  end
end
$ iex math.exs
iex> Math.double_each([1, 2, 3]) #=> [2, 4, 6]

Here we have used recursion to traverse a list, doubling each element and returning a new list. The process of taking a list and mapping over it is known as a map algorithm.

Recursion and tail call optimization are an important part of Elixir. However, when programming in Elixir you will rarely use recursion as above. The Enum module, (next chapter), already provides many conveniences for working with lists. For instance, 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]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment