Nithin Bekal About

Notes / Elixir

brew install elixir
  • dynamic/functional lang
  • runs on Erlang VM
  • Actor model - each actor is a separate process in the VM - allows concurrency

Processes are lightweight, and exchange info via messages. This isolates processes, which allows independent GCs and prevents system-wide pauses.

Basic types and operations

40 + 2
"hello" <> " world"     # string
:atom                   # atom/symbol
[1, 2, 3]               # list
{1, 2, 3}               # tuple
[1, 2] ++ [3, 4]        # [1, 2, 3, 4]

# Use / for float and div for int division
# parenthesis are optional when invoking functions
# Floats are 64 bit double precision numbers
10 / 2      # 5.0
div 10, 2   # 5
rem 11, 3   # 2
  • Booleans:
is_boolean(true)     # true
is_boolean(false)    # false

# true and false are same as atoms :true and :false
is_atom(false)       # true
is_boolean(:true)    # true
is_boolean(:false)   # false
  • Functions are referred by name and arity, eg. is_boolean/1.

  • Strings are UTF-8 encoded

"hello
world"            # "hello\nworld"
IO.puts "hello"   # Prints "hello" and returns :ok
  • When using binary operations, use and, or, not when first argument is boolean.
  • With &&, ||, ! the args can be of any type. (Everything except false and nil eval to true, just like in Ruby)
false or is_atom(:example)  # true
true and true               # true
1 and true                  # ArgumentError
false and error("This error will never be raised")

1 || true       # 1
2 && 3          # 3
false || 11     # 11
!1              # false
!nil            # true
  • Comparison operators: === is more strict compared to ==.
1 == 1.0        # true
1 === 1.0       # false
  • Different data types can be compared. This allows sorting algorithms to not worry about data types.
1 < :atom       # true

# sorting order
number < atom < reference < functions < port < pid < tuple < maps < list < bitstring

Pattern matching

  • = is the pattern match operator
x = 42
x               # 42
42 = x          # fine
1 = x           # ** (MatchError) no match of right hand side value: 42
2 = foo         # ** (RuntimeError) undefined function: foo/0
  • x = 42 assigns 42 to the variable x.
  • 42 = x is ok because the operator only checks if both sides match
  • 1 = x fails because the right side has the value 42 and left side has the value 1, and so the match fails
  • Variables can only be assigned if they are on the left side
  • When we try matching an unknown variable on the right side, Elixir thinks it is a function call to foo
  • You can re-bind variables
{ :ok, status } = { :ok, 400 } } }
status          # 400

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

[head | tail ] = [1, 2, 3]
head            # 1
tail            # [2, 3]
[ 4 | tail ]    # [4, 2, 3]
  • ^ (pin operator) lets you access previously bound values
  • it can be used when you need to match against a variable’s value prior to the match
x = 1
{ x, ^x } = { 2, 1 }
x   # 2

Strings, binaries, char lists

string = "hello"
is_binary string # true

# UTF8 strings
s = "hełło"
byte_size s             # 7
s |> String.length      # 5

# each character is a 'code point' whose
# value can be accessed via ? operator
      # 322
?a      # 97

# Binaries
<<1, 2, 3>>     # this is a binary
<<255>>         # max balue for binary
<<256>>         # 0  -- truncated

# Char lists
s = "hełło"
is_list s       # true
to_char_list s  # [104, 101, 322, 322, 111]
  • Char lists are mainly used for interfacing with old Erlang libs

Keyword lists

  • Keyword lists are similar to hashes/dictionaries in other languages
  • They map to arrays of key-value tuples
  • have 3 characteristics
    • keys must be atoms
    • keys are ordered
    • duplicate keys can exist
list = [a: 1, b: 2]
list == [{:a, 1}, {:b, 2}]  # true  -- same thing
list[:a]                    # 1
list ++ [c: 3]              # [a: 1, b: 2, c: 3]

# can have same key repeated
list2 = [a: 0] ++ list
list2[:a]               # 0  -- values in front override others

# Keyword list based if macro
if false, do: 1, else: 2

# Pattern matching requires number of elements and order to match
[a: a] = [a: 1]
a  # 1

Maps

  • Key value store
map = %{ :a => 1, 2 => :b }
map[:a]         # 1
map[2]          # :b
Map.get map, :a # 1
map.a           # 1

# pattern match
%{:a => a} = map
a               # 1

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

Modules and functions

  • The defmodule macro defines a module
  • def defines a function
  • Functions have implicit returns
defmodule Math
  def sum(a, b) do
    a + b
  end
end

Fibonacci example:

defmodule Fibonacci do
  def fib(0), do: 0
  def fib(1), do: 1
  def fib(n), do: fib(n-1) + fib(n-2)
end

This version, using case, maps to the pattern matching version at the VM level.

def fib(n)
  case n do
    0 -> 0
    1 -> 1
    n -> fib(n-1) + fib(n-2)
  end
end

Ruby-like version:

def fib(n) do
  if n < 2
    n
  else
    fib(n-1) + fib(n-2)
  end
end

Length of a list:

def len([]), do: 0
def len([h|t]), do: 1 + len(t)

Map:

def map([],    f), do: []
def map([h|t], f), do: [ f.(h) | map(t, f) ]

Run length encoding:

defmodule RLE do
  def encode(list), do: _encode(list, [])

  defp _encode([], result), do: Enum.reverse(result)

  defp _encode([a | a | t], result) do
    _encode([ {a,2} | tail ], result)
  end

  defp _encode( [ {a, n}, a | tail ], result ) do
    _encode( [ { a, n+1 } | tail ], result )
  end

  defp _encode( [a | tail ], result ) do
    _encode(tail, [ a | result ])
  end
end

Default args:

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

Processes

  • Achieves concurrency by making use of actors.
  • Actors are processes that can perform a specific task.
  • We can send messages to an actor to ask it to do something, and it can respond by sending back a message.

Flow example

From Jose’s Elixirconf 2016 Keynote:

Flow.stream("file_name", :line)
|> Flow.from_enumerable()
|> Flow.flat_map(&String.split/1)
|> Flow.partition()
|> Flow.reduce(%{}, fn word, map ->
  Map.update(map, word, 1, &(&1 + 1))
end)
|> Enum.into(%{})

Umbrella apps

Ecto

Plug

Links

OTP

Videos

Books