Elixir

Strings

Concatenation

"hello " <> "world"

Interpolation

"hello #{world}"
"5 + 10 = #{5 + 10}"

Sigils

~s("Includes quotes")
~S("raw backslash char: \") # doesn't escape

Charlists

Lists of ASCII codepoints (range 0-127 I think)

[65, 66, 67]
# ~c"ABC"

IO

IO.puts("hello world")

Types

[1, 2, 3] # list
{1, 2, 3} # tuple

Functions

oneline functions

def area(a, b) do: a * b

Default Values

def area(a, b \\ 0) do: a * b

Logic

true and true
true or true

Data Structures

Lists

[1, 2, 3] ++ [4, 5, 6] # concatenation (O(len(first list)) bc linked lists)
hd(list) # head of the list
tl(list) # the rest of the list

5 in list # if an elem is in a list

List.replace_at(list, 0, 1)

List.insert_at(list, 0, 1)

Lists & tuples can hold multiple values with different types

Lists are stored as singly-linked lists

Generally add only to the front of a list

Modifying Lists

  • When you modify a list, the new version contains a shallow copy of the first n - 1 entries with the tail after that shared

Keyword Lists

Linked list of key value pairs. Useful for smaller maps

[monday: 1, tuesday: 2] # equals a list of Tuples with atom keys

These are often used for named arguments, so often that you can omit the square brackets

IO.inspect([100, 200, 300], width: 3, limit: 1)

Tuples

elem(tup, 1) # first element

tup = put_elem(tup, 1, 26) # update index 1 to 26

Modifying Tuples

When you modify a tuple, it returns a shallow copy of the old tuple with the modification

Semantics

  • the function size is used if the value is stored in the data structure (linear time)
    • Or named length if it needs to be computed

Maps

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

# This is equivalent to:
map = %{a => 1, b => 2}

map[:a]

# or
Map.get(map, :a, default)
#or
map.a # works for atom keys

Map.fetch(:a)
# returns {:ok, 1} or :error

Map.put(map, :c, 3)

# Updating a field
map = %{map | c: 10}

MapSets

Use as your default set implementation

MapSet.new([:monday, :tuesday, :wednesday])
MapSet.member?(days, :monday)
# true

Datetimes

date = ~D[2023-01-31]
date.year

Pattern Matching

The equals sign operator is the match operator

x = 1 returns true and then so will 1 = x

Destructuring

{a, b, c} = {:hello, :world, 42}

# assigns result if :ok
{:ok, result} = func

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

# use this to prepend
list = [0 | list]

# matching maps
%{name: name, age: age} = bob

# matching binaries
<<b1, b2, b3>> = "ABC"

<<b1, rest :: binary>> = binary

# chaining matches
a = (b = 1 + 3) # parens are optional
# a = 4

Pin Operator

Does a match without assignment

x = 1
^x = 2 # no match

Case

x = 4
case {1, 2, 3} do
  {^x, 2, 3} ->
    "no match because x is pinned"
  {1, x, 3} ->
    "x gets reassigned to 2"

   _ when x > 0 ->
    "default"
end

Errors in guards don't get thrown. They just don't match

Logic

Cond

Use cond to handle branching conditionals

cond do
  2 + 2 == 5 ->
    "won't match"
  1 + 1 == 2 ->
    "but this will"
  true ->
    "default"
end

Guards

def sign(x) when is_number(x) and x < 0 do
  :negative
end

def sign(x) when is_number(x) and x > 0 do
  :positive
end

def test(0) do
  :zero
end

Guards with Lambdas

test_num = fn
  x when is_number(x) and x < 0 -> :negative
  x when is_number(x) and x > 0 -> :positive
  0 -> :zero
end

unless keyword

if not

unless result == :error do: # ...

with

with is useful for having chaining expressions returning {:ok, result} or {:error, reason}

Once it encounters an {:error, reason}, it'll return {:error, reason}

with {:ok, login} <- get_login(),
     {:ok, email} <- get_email(),
     {:ok, password} <- get_password(),
{:ok, %{login: login, email: email, password: password}}

Functions

Anonymous Functions

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

Capture Operator

& captures functions. &1 references the first parameter

fun = &(&1 + 2)

fun = &(&1 + &2) # 2-arity function

# use for function references
&add/2

# note that this is still creating an anonymous function, thus this is valid
Enum.each(1..5, &fun(&1 + 2))

Naming Conventions

Postfix in ? if it returns a bool

Recursion, reductions

You can match the parameters of a function. But this will iterate over each instance that matches the arity.

e.g. if you provide three matches for area/1 it won't iterate over them for a call to area() with 2 parameters

defmodule Math do
  def sum_list([head | tail], accumulator) do
    sum_list(tail, head + accumulator)
  end

  # pattern match the base case
  def sum_list([], accumulator) do
    accumulator
  end
end

IO.puts(Math.sum_list([1, 2, 3], 0))

Enum.map([1, 2, 3], &(&1 * 2))
Enum.reduce([1, 2, 3], &+/2)

Elixir compiles head | tail recursions to something resembling gotos (e.g. in a traditional for loop) This is true for all tail recursive calls - where the last thing in the function is the recursive call

Streams vs. Enums

  • streams are lazy

    Enum.to_list(stream)
    
    Enum.take(stream, 10) # get the first 10 results
    
    Enum.each(stream, func)

Enums

Enum.each(list, func)

Enum.map(list, func)

Modules

defmodule Circle do
  @pi 3.14 # compile time constant
end

Type Hints

  • Called type specs

    @spec area(number) :: number
    def area(r) do: r * r * @pi

Binaries, Bitstrings

  • Binary - a collection of bytes

    <<255>> # 255
    <<256>> # overflows to 0
    
    <<255::16>> # specify to use 16 bits for 255
    # <<0, 255>>
    
    <<257::16>>
    # <<1, 1>> because this represents 0x01 0x01
  • The result of a binary is comma-separated sequences of 8 bits

  • If the result isn't in a multiple of 8 bits, it's a bitstring

Comprehensions

Iterates over the input list and returns the list w/ the function applied

for x <- [1, 2, 3] do
    x * x
end

# can use ranges
for x <- 1..3 do
end

multiplication_table =
    for x <- 1..9,
      y <- 1..9,
      x <= y, # filter
  into: %{} do
        {{x, y}, x * y}
    end

Structs

%Fraction{fraction | b : 4} # replace a field

Protocols

  • Analogous to interfaces

    defprotocol String.Chars do
      def to_string(term)
    end
    
    # for can be Tuple, Atom, List, Map, BitString, Integer, Float, Function
    defimpl String.Chars, for: Integer do
      def to_string(term) do
        res
      end
    end

Base protocols to implement include Enumerable, Collectable

BEAM

  • BEAM is built to abstract away processes inside of the main Erlang process. It abstracts away server-server communication as if it was process-process communication
    • e.g. instead of using a message queue and in-memory cache, everything can just be Elixir
    • the BEAM still doesn't replace the horizontally scalability you get from tools like K8s

Concurrency

  • Processes are managed by schedulers. By default, the BEAM allocates one scheduler for each available CPU thread

Concurrency

pid = spawn(fn -> ...)
# create a process, this returns the PID

send(pid, variable)

# on the receiver
receive do
  pattern_1 -> func()
  pattern_2 -> func2()
after
  5000 -> IO.puts("no message found after 5000 secs")
end

pid = self() # get the current process's PID

get_result =
fn ->
  receive do
    {:query_result, result} -> result
  end
end

Enum.each(1..5, fn _ -> get_result.() end)

Server Processes

  • Long-running server processes

    defmodule DatabaseServer do
      def start do
        spawn(&loop/0)
      end
    
      defp loop do
        receive do
          ...
        end
    
        loop() # tail recurse to loop
      end
    end

Stateful Processes

def start do
  spawn(fn ->
    initial_state = ...
    loop(initial_state)
  end)
end

defp loop(state) do
  ...
  loop(state)
end

Managing Several Processes

Register names with:

Process.register(self(), :some_name)

Misc

Make sure to match all in a receive block, otherwise they sit in the processes input queue

Ranges

range = 1..2
2 in range # true

Enum.each(1..2, func)

Misc

  • Integer division: div(5, 2)
  • Remainder: rem(3, 2)

Phoenix

LiveView

Url Params, Sessions

The session info comes from a signed cookie

def mount(%{"house" => house}, _session, socket) do
  Thermostat.get_house_temp(house)
end

# in the router:
live("/thermostat/:house")

Templates

<section :for={post <- @posts} :key={post.id}>
  <h1>{expand_title(post.title)}</h1>
</section>

Reactive variables:

assign(assigns, sum: assigns.x + assigns.y)