Octallium - Functional with Elx
Match Operator and Pattern Matching
Elixir uses the =
sign not for assignments but for matching both sides (left and right) into the same value.
It’s called the match operator
a = 1
1 = a
You can also match by patterns, it’s called pattern matching
[a, a] = [1, 1]
The following code however will not work because the match operator matches the value 1
into d
, and then uses it to match against 2
, which causes a match error between the right and left side
d = 1
2 = d
The same thing applies when matching against incompatible patterns, this will try to both math the values 1
and 2
into the same variable c
, which will cause a MatchError, and the value of c
does not change because all variables in Elixir are immutable by default.
# this will not work properly
try do
[c, c] = [1, 2]
rescue
e -> IO.inspect(e)
end
However if we try to match an variable twice, we actually can change the value of it:
immutable_variable = 1
immutable_variable = 20
This happens because Elixir thinks that we want to bound the new value to the variable on the left, for this to not happen, we can use the pin (^
) operator:
# which is equivalent to "10 = immutable_variable"
try do
^immutable_variable = 10
rescue
e -> IO.inspect(e)
end
This immutability allows for applications to easily scale operations on these values, since we can copy easily this value across different threads/cores of an actual computer
Actor Model
An actor receives a message and can process it and send message to other actors or emit a response. It’s considered an isolated unit that gets something, process it and gives back.
On the Erlang VM, the run inside processes which in turn exchanges immutable data.
And the Erlang VM can be run on a cluster of machines, which means that we can scale horizontally across the globe if we want.
These processes are not OS processes, they are more similar to virtual threads.
- Actors run in processes which are isolated from each other
- Each process is identified by a PID (Process Identifier)
- Allows inter-communication of processes by message passing
- Since data is immutable, we do not have to worry about changing state between actors, since all computation runs using copies of data
- Each process has it’s Stack & Heap allocation, meaning each process has it’s own GC and exceptions does not halt the entire program (only a process)
How processes works
- Receives messages in mailbox
- Executed FIFO
- Very cheap to create (less than 3kb memory)
- Always communicate by message passing
When using iex
, we have an interactive repl which itself is a process
We can check the PID of the running process by using the builtin function self/0
self()
Basics
Elixir have two ways to run code in projects, interpreted (frequently used for scripts) and compiled.
interpreted will pass line by line and execute it instantly on the Erlang VM.
compiled will convert the code into bytecode (.beam
file), which in turn can be executed directly on the Erlang VM.
The main difference is the speed, when running in interpreted mode, Elixir have to do two steps and thus it’s slower, however if you run directly the bytecode, this will be much faster.
Also, scripting is frequently used when we don’t want that code to be compiled together with the application when deploying, such as database migrations, testing etc…
To run in interpreted mode, name your files with .exs
extension.
For compiling, name with .ex
extension.
Functions in Elixir are identified by arity (number of arguments) also, so we can have two functions with the same name but with different arity.
Data Types - Atoms
Represents an symbol for something, it’s a variable which the name of the variable is the same as it’s value.
// in javascript
const nike = "nike"
# instead of writing
nike = "nike"
# we can use:
:nike
# here are some valid synthax for atoms
at = :an_atom
at2 = :"Also an atom but with spaces!"
at2
They are very useful for pattern matching, it’s common to use atoms such as :ok
and :error
such as below:
defmodule SomeLib do
@moduledoc """
iex> SomeLib.divide(5, 2)
{:ok, 2.5}
iex> SomeLib.divide(10, 0)
{:error, "cannot divide by 0"}
"""
def divide(dividend, divisor) when is_integer(dividend) and is_integer(divisor) do
if divisor == 0 do
{:error, "cannot divide by 0"}
else
{:ok, dividend / divisor}
end
end
end
{:ok, result} = SomeLib.divide(10, 2)
result
{:error, reason} = SomeLib.divide(5, 0)
reason
Data Types - String
If we use double quotes ("
), that is considered a String (or binary string) by Elixir such as above:
IEx.Info.info("Hi there")
However, single quotes ('
) produces a character list:
IEx.Info.info(~c"Hi again")
We can pattern match on Strings:
"H" <> rest = "Hi there"
# Elixir again tries to match LHS with RHS, thus bounding "i there" to `rest`
rest
The raw representation of Strings are binary values, inside double angle brackets:
an_string = <<72, 105>> <> " mom"
an_string
<> = an_string
head
<> = an_string
head
# is head equals to code point of "H"
head == ?H
rest
Data Types - Charlist
chars = ~c"World"
chars |> IEx.Info.info()
~c"Hello " ++ chars
is_list(chars)
Data Types - Process
my_pid = self()
my_pid
Data Types - Lists
Are actually considered Linked Lists (which theyselves are recursive in nature)
list = ["a", "b", "c"]
list
try do
list[0]
rescue
e -> IO.inspect(e)
end
# We receive error because this is actually a linked lists, thus index operations does not works
# We can access it through builtin functions of std lib
Enum.at(list, 0)
To show the documentation of the function we can run:
import IEx.Helpers
# in IEx console we can use just "h()"
IEx.Helpers.h(Enum.at())
[f, s, t] = list
IO.inspect(f)
IO.inspect(s)
IO.inspect(t)
list
# returns the head of list
hd(list)
# returns the tail of list
tl(list)
[h | t] = list
h
t
Data Types - Tuples
# similar to lists, but used for static few elements
{a, b} = {1, 2}
a
{:reply, msg, state} = {:reply, "Abe found", ["Abe", "John", "Mary"]}
msg
Data Types - Keyword List
an actual list but with key and value, where the key are atoms:
data = [a: 1, b: 2]
it’s exactly the same as using tuples inside lists:
[{:a, 1}, {:b, 2}] == data
data[:a]
You can find functions to manage this data type under Keyword
standard module of Elixir.
Data Types - Maps
Very similar to keyword lists, but offers more features such as using different data types for keys:
my_map = %{a: 1, b: 2, c: 3}
my_map
my_map.a
%{a: first, b: second, c: _} = my_map
second
%{c: third} = my_map
third
# we can use other data types as key
map2 = %{"first" => 1, "second" => 10}
# to extract elements from a map based on key
%{"second" => second_elem} = map2
second_elem
Map.get(map2, "first")
map2 = %{map2 | "first" => 2}
my_map = %{my_map | c: 20}
Data Types - Struct
Structs always take the name of the Module it’s in:
defmodule User do
defstruct username: "", email: "", age: nil
end
user1 = %User{username: "Abe", age: 20, email: "abe@email.com"}
%{username: username} = user1
username
user1 = %{user1 | age: 21}
Recursion
Avoid mutation of data, recursion is a common tool in functional programming for doing repetitive tasks.
Elixir offers recursion easily and has builtin optimization in the compiler to avoid performance issues and high memory usage:
defmodule PrintDigits do
# Base case
def upto_asc(0), do: :ok
def upto_asc(num) when is_integer(num) do
cond do
num > 0 -> upto_asc(num - 1)
# recursive call happens first (Head Recursion)
num < 0 -> upto_asc(num + 1)
end
IO.puts(num)
end
# Base case
def upto_desc(0), do: :ok
def upto_desc(num) when is_integer(num) do
IO.puts(num)
cond do
num > 0 -> upto_desc(num - 1)
# recursive call happens last (Tail recursion)
num < 0 -> upto_desc(num + 1)
end
end
end
PrintDigits.upto_asc(5)
PrintDigits.upto_desc(5)
defmodule SumDigits do
def upto(0), do: 0
def upto(num) when is_integer(num) do
upto(num - 1) + num
end
def upto_tail_rec(num, acc \\ 0)
def upto_tail_rec(0, acc), do: acc
def upto_tail_rec(num, acc) do
upto_tail_rec(num - 1, acc + num)
end
end
SumDigits.upto(10_000_000)
By making a recursive function tail recursive, we can achieve much more performance, since the ELixir compiler can Reuse the Stack, instead of creating multiple instances.
SumDigits.upto_tail_rec(10_000_000)
defmodule Factorial do
def call(1), do: 1
def call(num) do
num * call(num - 1)
end
def call_rec(num, acc \\ 1)
def call_rec(1, acc), do: acc
def call_rec(num, acc), do: call_rec(num - 1, acc * num)
end
Factorial.call(100_000)
Factorial.call_rec(100_000)
defmodule ReverseNum do
def call(num, acc \\ 0)
def call(0, acc), do: acc
def call(num, acc), do: call(div(num, 10), acc * 10 + rem(num, 10))
end
ReverseNum.call(532)
Now let’s apply the recursion to lists!
defmodule ListsExercises do
@spec sum(list(number())) :: number()
def sum([]), do: 0
def sum([h | t]), do: h + sum(t)
def double_num(num), do: num * 2
def sum_rec(nums, acc \\ 0)
def sum_rec([], acc), do: acc
def sum_rec([h | t], acc), do: sum_rec(t, acc + h)
def reverse([]), do: []
def reverse([h | t]), do: reverse(t) ++ [h]
@spec reverse_rec([any()], [any()]) :: [any()]
def reverse_rec(elems, acc \\ [])
def reverse_rec([], acc), do: acc
def reverse_rec([h | t], acc), do: reverse_rec(t, [h | acc])
@spec map_rec([any()], (any() -> any()), [any()]) :: [any()]
def map_rec(elems, fun, acc \\ [])
def map_rec([], _, acc), do: acc |> reverse_rec()
def map_rec([h | t], fun, acc), do: map_rec(t, fun, [fun.(h) | acc])
@spec concat([any()], [any()]) :: [any()]
def concat(src, dst), do: concat_func(src |> reverse_rec(), dst)
defp concat_func([], dst), do: dst
defp concat_func([h | t], dst), do: concat_func(t, [h | dst])
def flat_map(elems, fun, acc \\ [])
def flat_map([], _, acc), do: acc
def flat_map([h | t], fun, acc) do
flat_map(t, fun, concat(acc, fun.(h)))
end
end
[1, 2, 2, 2, 2, 2, 8] |> ListsExercises.sum()
[1, 2, 2, 2, 2, 2, 8] |> ListsExercises.sum_rec()
["a", "b", "c"] |> ListsExercises.reverse()
["a", "b", "c"] |> ListsExercises.reverse_rec()
ListsExercises.map_rec([1, 2, 3, 4, 5], fn x -> x * x end)
ListsExercises.map_rec([a: 1, b: 2], fn {k, v} -> {k, -v} end)
ListsExercises.map_rec([1, 2, 3, 4, 5, 6], &ListsExercises.double_num/1)
# same as ++ operator
ListsExercises.concat([1, 2, 3], [4, 5, 6])
ListsExercises.flat_map([:a, :b, :c], fn x -> [x, x] end)
defprotocol ToRed do
@spec call(String.t()) :: String.t()
def call(value)
end
defimpl ToRed, for: BitString do
def call(self), do: self <> " but in red"
end
ToRed.call("abe")