Skip to content

Instantly share code, notes, and snippets.

@sleepingkingstudios
Last active August 25, 2021 21:39
Show Gist options
  • Save sleepingkingstudios/73fd68920684c3a1d253b9856a7284ff to your computer and use it in GitHub Desktop.
Save sleepingkingstudios/73fd68920684c3a1d253b9856a7284ff to your computer and use it in GitHub Desktop.
Elixir Cheat Sheet

Elixir Cheat Sheet: Organizing Code

Control Flow

case Statements

See case Statements, Case.

case allows us to compare a value against many patterns until we find a matching one.

cond Statements

See Cond.

In many circumstances, we want to check different conditions and find the first one that does not evaluate to nil or false. In such cases, one may use cond:

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 all of the conditions return nil or false, an error (CondClauseError) is raised. For this reason, it may be necessary to add a final condition, equal to true, which will always match. Note cond considers any value besides nil and false to be true.

if and unless Statements

See If and Unless.

Elixir also provides the macros if/2 and unless/2 which are useful when you need to check for only one condition. If the condition given to if/2 returns false or nil, the body given between do/end is not executed and instead it returns nil. The opposite happens with unless/2.

if true do
  "This works!"
end #=> "This works!"

unless true do
  "This will never be seen"
end #=> nil

if nil do
  "This won't be seen"
else
  "This will"
end #=> "This will"

do/end Blocks

See do/end Blocks.

do/end blocks are a syntactic convenience built on top of keyword lists. These are equivalent:

if true do
  a = 1 + 2
  a + 10
end #=> 13

if true, do: (
  a = 1 + 2
  a + 10
) #=> 13

One thing to keep in mind when using do/end blocks is they are always bound to the outermost function call.

Elixir Cheat Sheet: Pattern Matching

Pattern Matching

See Pattern Matching.

The Match Operator: =.

When the sides do not match, a MatchError is raised.

Variables can be assigned from the left side of the match, e.g. x = 1.

Although pattern matching allows us to build powerful constructs, its usage is limited. For instance, you cannot make function calls on the left side of a match.

Matching Data Structures

Matching Tuples:

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

A pattern match error will occur if the sides can’t be matched, for example if the tuples have different sizes, or when comparing different types.

Matching Lists:

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

A list also supports matching on its own head and tail. Similar to the hd/1 and tl/1 functions, we can’t match an empty list with a head and tail pattern.

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

[head | tail] = [] # (MatchError) no match of right hand side value: []

Matching Maps:

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

n = 1 #=> 1
map = %{n => :one} #=> %{1 => :one}
map[n] #=> :one
%{^n => :one} = %{1 => :one, 2 => :two, 3 => :three} #=> %{1 => :one, 2 => :two, 3 => :three}

Matching Specific Values

{:ok, result} = {:ok, 13}
result #=> 13

{:ok, result} = {:error, :oops} # (MatchError) no match of right hand side value: {:error, :oops}

The Pin Operator

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

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

We can use the pin operator inside other pattern matches, such as tuples or lists:

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

If a variable is mentioned more than once in a pattern, all references should bind to the same value.

The variable _ is special in that it can never be read from.

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

Matching case Statements

See Case.

case allows us to compare a value against many patterns until we find a matching one.

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"

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

To match any value, use the _ -> clause.

If none of the clauses match, an error is raised.

case :ok do
  :error -> "Won't match"
end # (CaseClauseError) no case clause matching: :ok

Matching Bitstrings, Binaries, and Strings

See documentation for Binaries.

<<0, 1, x>> = <<0, 1, 2>> #=> <<0, 1, 2>>
x #=> 2

<<0, 1, x>> = <<0, 1, 2, 3>> # (MatchError) no match of right hand side value: <<0, 1, 2, 3>>

Unless you explicitly use :: modifiers, each entry in the binary pattern is expected to match a single byte (exactly 8 bits). If we want to match on a binary of unknown size, we can use the binary modifier at the end of the pattern.

<<0, 1, x :: binary>> = <<0, 1, 2, 3>> #=> <<0, 1, 2, 3>>
x #=> <<2, 3>>

The binary-size(n) modifier will match n bytes in a binary.

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

When pattern matching on strings, it is important to use the utf8 modifier:

<<x::utf8, rest::binary>> = "über" #=> "über"
x ==  #=> true
rest #=> "ber"

Guards

See documentation for Guards.

Clauses also allow extra conditions to be specified via guards.

case {1, 2, 3} do
  {1, x, 3} when x > 0 ->
    "Will match"
  _ ->
    "Would match, if guard condition were not satisfied"
end #=> "Will match"

Errors in guards do not leak but simply make the guard fail.

Elixir Cheat Sheet: Types and Operators

Basic Types

See Basic Types.

  • Integer: 1, 0x1F
  • Float: 1.0
  • Boolean: true, false (also :true, :false)
  • Atom: :atom
  • String: "elixir" (note double quotes!)
  • Charlist: 'hello' (equivalent to [104, 101, 108, 108, 111]) (not equal to "hello")
  • List: [1, 2, 3]
  • Tuple: {1, 2, 3}

Bitstrings

See documentation for Bitstrings.

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

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, or you can use the more verbose declaration ::size(n).

<<42>> === <<42::8>> #=> true

<<3::4>> #=> <<3::size(4)>>

Any value that exceeds what can be stored by the number of bits provisioned is truncated.

Binaries

See documentation for Binaries.

A binary is a bitstring where the number of bits is divisible by 8. Every binary is a bitstring, but not every bitstring is a binary. See the is_bitstring/1 and is_binary/1 functions.

Strings

A string is a UTF-8 encoded binary, where the code point for each character is encoded using 1 to 4 bytes. Thus every string is a binary, but due to the UTF-8 standard encoding rules, not every binary is a valid string.

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

?a #=> 97
 #=> 322

Charlists

See documentation for Charlists.

A charlist is a list of integers where all the integers are valid code points. Whereas strings (i.e. binaries) are created using double-quotes, charlists are created with single-quoted literals:

'hełło' #=> [104, 101, 322, 322, 111]
is_list 'hełło' #=> true

'hello' #=> 'hello'
List.first('hello') #=> 104

You can convert a charlist to a string and back by using the to_string/1 and to_charlist/1 functions:

Operators

See Basic Operators.

Arithmetic Operators: +, -, *, /, plus the functions div/2 and rem/2 for integer division and remainder.

Binary / String Operators: <> to concatenate.

List Operators:

++/2 to concatenate, --/2 to subtract.

Use the | operator to prepend:

list = [1, 2, 3]
[0 | list] #=> [0, 1, 2, 3]

Comparison Operators:

Elixir also provides ==, !=, ===, !==, <=, >=, < and > as comparison operators. The difference between == and === is that the latter is more strict when comparing integers and floats.

Logical Operators:

Elixir also provides three boolean operators: or, and and not. These operators are strict in the sense that they expect something that evaluates to a boolean (true or false) as their first argument. Providing a non-boolean will raise an exception. or and and are short-circuit operators.

Elixir also provides ||, && and ! which accept arguments of any type. For these operators, all values except false and nil will evaluate to true.

Pin Operator: ^ prevents re-binding of variables when pattern matching. See Pattern Matching.

Functions

Functions in Elixir are identified by both their name and their arity.

The Elixir shell defines the h function, which you can use to access documentation for any function. For example, typing h round/1 is going to print the documentation for the round/1 function.

Anonymous Functions

Delimited by the keywords fn and end:

add = fn a, b -> a + b end
add.(1, 2)
is_function(add)

Note that a dot (.) between the variable and parentheses is required to invoke an anonymous function.

Anonymous functions can also have multiple clauses and guards. The number of arguments in each anonymous function clause needs to be the same, otherwise an error is raised.

f = fn
  x, y when x > 0 -> x + y
  x, y -> x * y
end

Data Structures

Elixir data structures are immutable.

When counting the 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).

Lists

See Linked Lists.

Elixir uses square brackets to specify a (linked) list of values. Lists have variable size but linear lookup.

[1, 2, true, 3]
length [1, 2, 3] #=> 3

The length of a list can be calculated using the length/1 function. This is a linear time lookup, proportional to the length of the list.

Two lists can be concatenated or subtracted using the ++/2 and --/2 operators respectively.

Lists of ASCII integers (0-255) may be represented as Charlists:

'hello' == [104, 101, 108, 108, 111] #=> true

Head and Tail

The head is the first element of a list and the tail is the remainder of the list. They can be retrieved with the functions hd/1 and tl/1.

list = [1, 2, 3]
hd(list) #=> 1
tl(list) #=> [2, 3]

Getting the head or the tail of an empty list throws an error.

Tuples

See Tuples.

Elixir uses curly brackets to define tuples. Tuples store elements contiguously in memory. This means accessing a tuple element by index or getting the tuple size is a fast operation. Indexes start from zero.

tuple = {:ok, "hello"}
elem(tuple, 1) #=> "hello"
tuple_size(tuple) #=> 2

The element at a given index can be accessed by the elem/2 function.

It is also possible to put an element at a particular index in a tuple with put_elem/3:

tuple = {:ok, "hello"}
put_elem(tuple, 1, "world") #=> {:ok, "world"}

Keyword Lists

See documentation for Keyword Lists.

A keyword list is list of tuples where the first item of the tuple (i.e. the key) is an atom. Elixir supports a special syntax for defining such lists: [key: value].

list = [{:a, 1}, {:b, 2}] #=>  [a: 1, b: 2]
list == [a: 1, b: 2] #=> true

Keyword lists have three special characteristics:

  • Keys must be atoms.
  • Keys are ordered, as specified by the developer.
  • Keys can be given more than once.

Keyword lists are the default mechanism for passing options to functions in Elixir. When the keyword list is the last argument of a function, the square brackets are optional. The following are all equivalent:

if false, do: :this, else: :that
if(false, [do: :this, else: :that])
if(false, [{:do, :this}, {:else, :that}])
#=> :that

Maps

See documentation for Maps.

Whenever you need a key-value store, maps are the “go to” data structure in Elixir. A map is created using the %{} syntax.

map = %{:a => 1, 2 => :b} #=> %{2 => :b, :a => 1}
map[:a] #=> 1
map[2] #=> :b
map[:c] #=> nil

Compared to keyword lists, we can already see two differences:

  • Maps allow any value as a key.
  • Maps’ keys do not follow any ordering.

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

n = 1 #=> 1
map = %{n => :one} #=> %{1 => :one}
map[n] #=> :one
%{^n => :one} = %{1 => :one, 2 => :two, 3 => :three} #=> %{1 => :one, 2 => :two, 3 => :three}

Maps have the following syntax for updating a key’s value. This requires the given key to exist - it cannot be used to add new keys.

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

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

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

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

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

Finally, maps provide their own syntax for accessing atom keys:

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

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

Nested Data Structures

See documentation for Nested Data Structures.

Elixir provides conveniences for manipulating nested data structures via the put_in/2, update_in/2 and other macros.

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

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

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

Elixir also defines get_and_update_in/2 to extract a value and update the data structure at once, as well as put_in/3, update_in/3 and get_and_update_in/3 which allow dynamic access into the data structure. See the Kernel module.

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