Elixir

(learning by observing)

Erlang

hot code reload

pattern matching

behaviours

protocol

typespecs

processes

Supervisor

GenServer

macro

scheduler

BEAM

mailbox

pid

Registry

OTP

special forms

mix

distilery

node

cluster

via

monitor

:observer

Agent

Task

ETS

Mnesia

umbrella

(un)quote

hex

binaries

with

capture

multiple heads

|>

let is crash

tail call optimization

guards

struct

use

pin

Scalable

  • code runs in lightweight processes
  • communication via asynchronous messages
  • immutable state not shared between processes
  • processes cooperate to provide complete service
  • VM can efficiently parallelise process execution across all available cores(vertical scaling)
  • processes can easily communicate with processes on other machines(horizontal scaling) 

Distributed

  • communication between processes works the same way no matter if they communicate locally on the same machine or over the network on separate nodes
  • adds to the overall resiliency of the system, when one nodes crashes, others can take over the load

Fault tolerant

  • processes are completely independent
    • state is not shared
    • error in one process doesn't affect other processes in the system
  • isolating error effects using supervisors
  • generally speaking, error in one part of the system should never bring down the whole system 

BEAM scheduling

BEAM process

Scheduler  #1

Scheduler #2

Scheduler #3

Scheduler #4

Setup

[vrabac - ~]$ iex
Erlang/OTP 20 [erts-9.0] [source] [64-bit]

Interactive Elixir (1.6.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>

Erlang interop

  • access to all Erlang standard libraries
  • call to Erlang modules must be prefixed with colon(:)
  • no peformance penalty for calling Erlang functions
iex(1)> :crypto.rand_uniform(1, 10)
6

Running scripts

# main.exs
a = 1
b = 2

IO.inspect("#{a + b}")

$> elixir main.exs
"3"
  • by convention, one time scripts are stored in .exs files

Interactive shell

# clock.exs
defmodule Clock do
  def now() do
    IO.inspect DateTime.utc_now()
  end
end

$> iex clock.exs
Erlang/OTP 20 [erts-9.0] [source] [64-bit]

Interactive Elixir (1.6.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Clock.now
#DateTime<2019-03-31 05:44:40.524135Z>

Recompile

[vrabac - running-scripts]$ iex clock.exs
Erlang/OTP 20 [erts-9.0] [source] [64-bit]

Interactive Elixir (1.6.1) - press Ctrl+C to exit (type h() ENTER for help)

# Compile a file
iex(2)> c "clock.exs"
warning: redefining module Clock (current version defined in memory)
  clock.exs:1

[Clock]

# Recompile and reload a module
iex(3)> r Clock
warning: redefining module Clock (current version defined in memory)
  clock.exs:1

{:reloaded, Clock, [Clock]}

Basic data types

# integer
1

# float
1.0

# atom
:name

# boolean - atom behind the scene
true || false
false == :false

# string: UTF-8 encoded binary
"hi"

# charlist: list of code points
'hi' == [104, 105]
# => true
# list
[1, 2, 3]

# tuple
{:ok, 1}

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

# anonymous function
square = fn(n) -> n * n end

square.(3)
# => 9

# PID
spawn(fn() -> 1 + 1 end)
# => #PID<0.132.0>

# Ref
spawn_monitor(fn() -> 1 + 1 end)
{#PID<0.90.0>, #Reference<0.919.270.234>}

Lists

  • implemented as linked lists
# linked list syntax
[1]       = [1 | []]
[1, 2]    = [1 | [ 2 | []]]
[1, 2, 3] = [1 | [ 2 | [3 | []]]]

# concatenation
[1, 2] ++ [3, 4]
#=> [1, 2, 3, 4]

Maps

# map literal
user = %{first_name: "harry", last_name: "hole"}

# update field(s)
user = %{user | first_name: "marry"}
# => %{first_name: "marry", last_name: "hole"}

# dot access notation
user.first_name
# => "marry"
  • key-value data structure
  • no restriction on key type, anything can be a key
  • dot notation can be used to access atom keys

Modules

defmodule Stack do
  def new() do
    []
  end

  def push(stack, item) do
    [item | stack] 
  end

  def pop(stack) do
    [_head | tail] = stack
    tail
  end
end

stack = Stack.new()

stack = Stack.push(stack, 1)
stack = Stack.push(stack, 2)
Stack.pop(stack)
# => [1]

Pipe operator

  • passes result of the "left" expression as the first argument to the function on the "right" side
Stack.new()
|> Stack.push(1)
|> Stack.push(2)
|> Stack.pop()
# => [1]

Stack.pop(
  Stack.push(
    Stack.push(
      Stack.new(),
      1
    ),
    2
  )
)

Functions - clauses

defmodule Util do
  def empty_array?([]) do
    true
  end

  def empty_array?(_) do
    false
  end
end
  • functions can have multiple clauses
  • given set of arguments, elixir will try to invoke each one in order they are declared until matching one is found, or raise an error

Functions - defaults

defmodule Util do
  def inc(n, step \\ 1) do
    n + step
  end
end
  • functions support default arguments

Functions - guards

defmodule Math do
  def positive?(n) when (is_float(n) or is_integer(n)) and n > 0 do
    true
  end

  def positive?(_n) do
    false
  end
end
  • guards augment pattern matching by allowing more complex validation of input arguments
  • guards allow only subset of expressions

Structures

defmodule User do
  defstruct [:first_name, :last_name]

  def with_first_name(user, name) do
    %{user | first_name: name}
  end
end

user = %User{first_name: "Harry", last_name: "Hole"}
# => %User{first_name: "Harry", last_name: "Hole"}

john = User.with_first_name(user, "John")
# => %User{first_name: "John", last_name: "Hole"}

john.first_name
# => "John"

Map.get(john, :first_name)
# => "John"
  • underneath is a normal map

Match operator

  • if posible, matches right side with the left side of the expression
  • as consequence, assigns value to a variable
1 = 1

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

a = 1
a
# => 1

a = 2
a
# => 2

3 = a
# => ** (MatchError) no match of right hand side value: 2

Pattern matching

  • ability to detect internals of the data structure
  • allows binding value to a variable(destructuring)
# overloading
def divide(l, 0) do
  :error
end

def divide(l, r) do
  l / r
end

# destructuring
{:ok, %Goth.Token{token: token}} = token.for_scope(scope)

# branching
case do_something() do
  {:success, result} ->
    # ...
  {:error, reason} ->
    # ...
end

Concurrency

  • concurent execution is achieved by running logic in multiple processes
  • processes are created using Kernel.spawn
$> pid = spawn(fn() -> IO.puts "Hi" end)
Hi
#PID<0.340.0>

$> Process.alive?(pid)
$> false

Communication

  • processes communicate via asynchronous messages
  • arguments are deep copied
me = self()

pid = spawn(fn() ->
  send(me, "hi")
end)

receive do
  message ->
    IO.inspect "Received: #{message}"
end

Links

  • links create bi-directional dependency between processes
  • crash in process will bring down all connected processes
  • links are created via Kernel#spawn_link
iex(26)> spawn_link(fn() -> 1/0 end)
** (EXIT from #PID<0.96.0>) shell process exited with reason: an exception was raised:
    ** (ArithmeticError) bad argument in arithmetic expression
        :erlang./(1, 0)


17:04:55.141 [error] Process #PID<0.141.0> raised an exception
** (ArithmeticError) bad argument in arithmetic expression
    :erlang./(1, 0)
Interactive Elixir (1.6.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>

Trapping exits

  • when trapping exits, crashes in other processes(links) are converted to messages
iex(4)> Process.flag(:trap_exit, true)
true
iex(5)> spawn_link(fn() -> 1/0 end)
#PID<0.150.0>

17:06:06.437 [error] Process #PID<0.150.0> raised an exception
** (ArithmeticError) bad argument in arithmetic expression
    :erlang./(1, 0)
iex(6)> flush
{:EXIT, #PID<0.150.0>, {:badarith, [{:erlang, :/, [1, 0], []}]}}
:ok

Monitors

  • monitors create uni-directional connection between processes
  • usefull when parent process wants to be notified about lifecycle events in children
iex(1)> spawn_monitor(fn() -> 1/0 end)
{#PID<0.98.0>, #Reference<0.2335281372.2063597571.182390>}
iex(2)>
17:20:34.543 [error] Process #PID<0.98.0> raised an exception
** (ArithmeticError) bad argument in arithmetic expression
    :erlang./(1, 0)

nil
iex(3)> flush
{:DOWN, #Reference<0.2335281372.2063597571.182390>, :process, #PID<0.98.0>,
 {:badarith, [{:erlang, :/, [1, 0], []}]}}
:ok

Wallet

defmodule Wallet do
  defstruct [:uid, :transactions]

  def new(uid) do
    %Wallet{uid: uid, transactions: []}
  end

  def withdraw(wallet, amount) when amount > 0 do
    %{wallet | transactions: [-amount | wallet.transactions]}
  end

  def deposit(wallet, amount) when amount > 0 do
    %{wallet | transactions: [amount | wallet.transactions]}
  end

  def balance(wallet) do
    Enum.sum(wallet.transactions)
  end

  def transactions(wallet) do
    Enum.reverse(wallet.transactions)
  end
end

Ruby vs Elixir

Learning curve

Syntax

Performance

Concurency

Ecosystem

Documentation

Ruby vs Elixir

Productivity

Maintainability

Debugging

Refactoring

Deployment

Testing

DSL

I'am the last slide

Questions?

Elixir

By vrabac

Elixir

  • 532