---
title: Learning Elixir, my thoughts and study notes
date: 2023-02-20
description: How and why you should Learn Elixir
---
# Learning Elixir: Why?
Why am I learning Elixir?
The short answer is: I have to know some Elixir for my current job, but I actually ended up really liking it. Elixirs functional programming paradigm reminds me of my first programming language, R, which brings back nostalgic memories. Although I initially learned R without knowing what a functional language was, Elixir is helping me appreciate the beauty of functional programming and how to create abstractions with just functions, without the need for mutable objects, methods, or classes.
Elixir's syntax is concise and elegant, and it comes with several impressive features, such as pattern matching, which is explained further in this blog. One of the main draws of Elixir for me is that it's built on Erlang, making it incredibly robust and highly concurrent, which I find really appealing. Additionally, Elixir has good documentation that is very accessible just like R, and it comes built-in with the language. The fact that Elixir has built-in pipes (`|>`) like Linux or R, is also something that any R enthusiast can appreciate.
# Learning Elixir: Where to start?
The main resource for learning is the official [Getting Started](https://elixir-lang.org/getting-started/introduction.html).
You can find everything there.
I am currently reading 2 books:
* [Learn Functional Programming With Elixir](https://www.amazon.com/Learn-Functional-Programming-Elixir-Foundations/dp/168050245X)
* [Elixir in action](https://www.manning.com/books/elixir-in-action-second-edition)
And I like them both.
And here you can find an [Elixir style guide](https://github.com/christopheradams/elixir_style_guide) so you can code in style.
# Learning Elixir: My notes
When learning a new programming language, the first thing I like to learn is:
* What is the language about?
* What are the most important data types?
* What are the semantics of the language?
When you know these things, you can effectively read the documentation and really get going.
I use these notes as a quick reference in case I forget something.
Its a good intermediate between the documentation and reading the official Elixir tutorial (which is good).
These notes are verbose enough so you have some context, but terse enough that you don't have to scroll through too much text.
# Elixir: A dense summary
Elixir is a functional language.
Elixir is dynamically typed.
All data types in elixir are immutable.
Copies are always returned because everything is immutable.
To reduce the copying overhead, Elixir creates copies in a smart way, so they seem like copies but under the hood they aren't.
Functions do not have side effects. They return their output, and do nothing else!
Check out this [video](https://www.youtube.com/watch?v=0if71HOyVjY&t=1743s) why that is something you want.
The Documentation of Elixir is amazing.
``` elixir
# Get documentation on function, modules, operators:
h Enum.each
h File
h case
```
All code can be run in an interactive shell: `iex`
# Elixir: My notes
Next are my notes where I summarize the very basics of Elixir.
My suggestion for following along: get the [raw markdown](https://raw.githubusercontent.com/trbKnl/my-personal-blog/main/blogs/learning_elixir.md) of these notes.
And run the examples in `iex`.
The examples try to show things that took me by surprise whenever I first encountered them.
What I personally do is, I use my own [vim plugin](https://niekdeschipper.com/projects/nvim_python_repl.md] in nvim to send code to `iex`)
Although I really like my own plugin, I am using it for Python, NodeJS, R for a long time already and have no problems with it.
But if you are a `(n)vim` user you should probably use something like [iron nvim](https://github.com/hkupty/iron.nvim), never tried it out, but probably does exactly the same thing as my plugin and much more.
# The = match operator
`=` is the match operator, it tries to bind the right-hand side to the left-hand side to make the statement true.
This is an very important feature of Elixir.
Examples:
```elixir
list = [1, 2, 3]
[a, b, c] = list
{_, {hour, _, _}} = :calendar.local_time # ignore a match with _
{_date, time} = :calendar.local_time # ignore a a match with _
[head | tail] = [1, 2, 3] # head has first item, tail the rest of the the list in a list
{:ok, file} = File.open("./myfile")
```
Pattern matches can be chained:
```elixir
a = (b = 3 + 1)
datetime = {_date, time} = :calendar.local_time
datetime
time
# This ensures x will be a map if the patternmatching succeeds
x = %{} = y
```
You can pattern match in function based on arguments:
``` elixir
defmodule Mul do
def mul({:two, a}) do
a * 2
end
def mul({:three, a}) do
a * 3
end
end
Mul.mul({:two, 2})
Mul.mul({:three, 2})
```
Ignoring a value with underscore:
``` elixir
[a, _b] = [1, 1]
[a, _] = [1, 1]
```
Variable can only be bounded once:
```elixir
[a, a] = [1, 1]
[a, a] = [1, 2] # does not work
```
Keep value of a fixed with caret called pin operator:
```elixir
a = 1
^a = 2
```
See this example
``` elixir
list1 = [1,2,3]
list2 = [4 | list1]
```
# Types in Elixir
The most important complex data types are: Map, Tuple, List.
The primitive types I skipped well as the `atom`.
Atoms are constants whose values are their own name.
They are easy to grasp, read more [here](https://hexdocs.pm/elixir/1.12/Atom.html).
## Map
Map is a key value store (dict in Python).
``` elixir
# Map declarations
%{:a => "c", :b => "b"}
%{a: "a", b: "b"}
a = %{a: "a", b: "b"}
a[:a]
a.a
# Check if map has key:
%{ a: _ } = %{ a: "aaa", b: "bbb" }
%{ c: _ } = %{ a: "aaa", b: "bbb" }
Map.has_key?(%{ a: "aaa"}, :a)
# Update the value of key in map
my_map = %{ a: "a", b: "b", c: "c" }
%{my_map | a: "aaa"}
```
## Tuple
Tuple module has function to work with tuples
``` elixir
person = {"Bob", 30}
elem(person, 0)
put_elem(person, 2, "Frikandel")
import Tuple
Tuple.append(person, "Alice")
```
## List
Lists are implemented as singly linked lists. Most operations are O(n).
``` elixir
person = ["Bob", "Alice"]
length(person)
# Enum is the go to module to work with enumerables
Enum.at(person, 1)
Enum.fetch(person, 1) # Returns {:ok, "Alice"}
Enum.fetch(person, 3) # Returns :error
Enum.fetch!(person, 5) # Throws Enum.OutOfBoundsError
[head | tail] = person # Used a lot in recursion
```
``` elixir
[1,2,3] ++ [4] # concatenation
[1,2,3] -- [1] # difference
1 in list # element in list
```
## Keyword Lists
An example how you can use default arguments:
``` elixir
# Keyword list
[aap: "aap", crocodile: "crocodile"]
# A keyword list just a normal list
[a: 1, b: 2] == [{:a, 1}, {:b, 2}]
```
If a keyword list appears as the last item in any context, you can leave the square brackets off.
This is useful for supplying arguments to a function.
This really tripped me up the first time I saw it.
```elixir
# Example of keyword list as the last item
# notice the lack of square brackets
fun.(x, y, a: "a", b: "b", c: "c")
```
## MapSet
Is the set data structure:
```elixir
x = MapSet.new()
x = MapSet.put(x, "ASD")
x = MapSet.put(x, "QWE")
x = MapSet.put(x, "QWE")
MapSet.member?(x, "QWE")
```
## Struct
Struct is a typed map. One module can only define a single struct:
```elixir
defmodule MyStruct do
# Keyword list provides fields and their initial values
defstruct a: "A", b: "B"
end
my_struct = %MyStruct{}
my_struct = %MyStruct{a: "a", b: "b"}
# Patternmatch a struct
%MyStruct{a: a, b: b} = my_struct
# Struct can't patternmatch a map
%MyStruct{a: a, b: b} = %{a: "a", b: "b"}
%{a: a, b: b} = %MyStruct{a: "a", b: "b"}
```
## String
```elixir
"abcdefg"
# format strings, string interpolation
x = "format strings, string interpolation"
"text: #{x}"
# <> is used for concatenating binaries, strings are binaries
"concatenate" <> "strings"
```
## Charlist
A list of characters:
```elixir
'abcdefg'
char_list = 'abcdefg'
is_list(char_list) == true
Enum.each(char_list, fn x -> IO.puts(x) end)
```
## Sigils
Sigils are custom functions that work on text.
They are useful, and a means to extend the language.
```elixir
# Create a list of words
h ~w{}
~w(the cat sat on the mat) # list with strings
~w(the cat sat on the mat)a # list with atoms
~w(the cat sat on the mat)c # list with charlists
# Create a regex
h ~r{}
my_regex = ~r/foo/
Regex.replace(my_regex, "foobar", "chill")
```
# Functions
Functions are defined by their name and arity (the number of input arguments).
## Anonymous functions
Anonymous function can be assigned to a variable.
Function can return functions, and can be used as arguments.
Functions are defined by their name and arity (the number of arguments a function has)
Example:
``` elixir
list = [1, 2, 3]
sum = fn list -> Enum.sum(list) end
sum.(list)
```
Single function can have multiple bodies.
I was blown away when I saw this at first.
The function body that matches the input argument is returned.
Conditional logic without if else statements!
``` elixir
fizz = fn
(0, 0, _) -> "FizzBuzz"
(0, _, _) -> "Fizz"
(_, 0, _) -> "Buzz"
(_, _, c) -> c
end
IO.puts(fizz.(0, 1, 1))
IO.puts(fizz.(1, 0, 1))
IO.puts(fizz.(0, 0, 1))
IO.puts(fizz.(1, 1, 1))
```
Anonymous functions have a shorthand:
```elixir
# Shorthand
fun1 = fn x -> x + 1 end
fun2 = &(&1 + 1)
fun1.(1) == fun2.(1) # 1 == 1
# You can capture functions
x = &IO.puts/1
x.("string")
```
## Named functions
Named functions have to be in modules:
```elixir
defmodule Times do
def double(n) do
n * 2
end
end
defmodule Times do
def double(n), do: n * 2
end
Times.double(2)
```
Functions can be pattern matched, it will try to match based on the function definition order.
This is a means to create conditional logic in your code, without if else statements.
Define from most specific to general
```elixir
defmodule Add do
def add(n) when is_integer(n), do: n + n
def add([x, y]), do: x + y
def add({a, b}), do: a + b
end
Add.add(2)
Add.add([2, 3])
Add.add({2, 3})
Add.add([]) # error, no match found
```
You can clause guard with "when" first patternmatch then guard clauses.
Check where guards can be used, its in quite a lot of places.
```elixir
defmodule Guard do
def what_is(x) when is_number(x) do
IO.puts "#{x} is a number"
end
def what_is(x) when is_list(x) do
IO.puts "#{inspect(x)} is a list"
end
def what_is(x) when is_atom(x) do
IO.puts "#{x} is an atom"
end
end
Guard.what_is(99)
Guard.what_is(:cat)
Guard.what_is([1,2,3])
```
Functions with an !, will raise an exception if the function encounters an error:
```
h File.stream!
```
`defp` defines a private function which can only be used in the module.
# Conditional logic
The most important conditional logic in Elixir: `case`, `cond` and pattern matching on function arguments.
## case: matches values
The `case` statement matches values!
``` elixir
result1 = {:ok, "lets, go!"}
result2 = {:error, "lets, go!"}
case_example = fn pattern ->
case pattern do
{:ok, str} -> IO.puts(str)
{:success, "we can continue"}
_ -> IO.puts("whomp, whomp") # _ matches anything
{:faillure, ":("}
end
end
case_example.(result1)
case_example.(result2)
```
## cond: matches conditions
`cond` matches conditions:
```elixir
x = 1
cond do
x == 1 -> "I am true"
x != 1 -> "I am false"
true -> :error # a catch all, because it always matches
end
```
## with: traverse a matching chain
`with` keeps evaluating, and enters the do block if all patterns match
If not match return that pattern: for example {:error, "reason why failed"}
```elixir
with {:ok, name} <- {:ok, "banaan"},
{:ok, login_id} <- {:ok, "banaan"} do
%{name: name, login_id: login_id}
end
```
## if else/unless
You have if and its inverse unless. For more than one else statement use `cond`.
``` elixir
compare_int = fn x ->
if x >= 1 do
:greater_than_or_equal_to_one
else
:less_than_one
end
end
compare_int.(0)
```
# Processing collections
## Enum: eager
Enum is the go to library to process collections.
Enum is eager.
```elixir
h Enum
list = [1, 2, 3]
Enum.filter(list, &(&1 != 2))
# Maps are also enumerables
# This example shows how maps are treated by enum
# Similar to [(k, v) for k, v in my_dictionary.items()] in Python
Enum.each(%{a: 1, b: 2}, fn {key, value} -> IO.puts("key #{key}, value #{value}") end)
# Also works for keyword lists
Enum.each([a: 1, b: 2], fn {key, value} -> IO.puts("key #{key}, value #{value}") end)
```
## Stream: lazy
Streams are lazy, they only deliver the next item in line when requested.
```elixir
my_stream = ["a", "b", "c", "d"]
my_stream
|> Stream.map(fn x -> x <> x end) # <> concatenates binaries ( asequence of bits divisible by 8): strings are binaries
|> Enum.take(1)
Stream.unfold(0, fn # check: h Stream.unfold
x -> {x, x + 1}
end)
|> Stream.map(fn x -> x + x end)
|> Enum.take(20)
```
## List comprehension
List comprehension exists!
It evaluates all combinations of multiple input sequences.
If conditions is `true` do something and return the result.
``` elixir
for a <- 1..10, a > 1 and a <= 4, do: a
for a when a < 5 <- 1..10, do: a
# Creative zip using list comprehension
my_zip = fn(list1, list2) ->
for {x_value, x_index} <- Enum.with_index(list1),
{y_value, y_index} <- Enum.with_index(list2),
x_index == y_index,
into: [] do
{x_value, y_value}
end
end
my_zip.([1,2,3], [3,2,1])
```