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

Atoms

  • Atoms are never garbage collected

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

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() do
  {:ok, %{login: login, email: email, password: password}}

with {:ok, json} <- Jason.decode(param),
    # ...

else
  {:error, reason} ->
  {:error_404, reason} ->

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

Use paths corresponding to module names. E.g. Todo.Server should go in lib/todo/server

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 (equivalent to 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

@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

Polymorphism

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

Behaviors

# The contract
defmodule URI.Parser do
  @doc "Defines a default port"
  @callback default_port() :: integer

  @doc "Parses the given URL"
  @callback parse(uri_info :: URI.t()) :: URI.t()
end

# The implementation
defmodule URI.HTTP do
  @behaviour URI.Parser

  @impl true
  def default_port(), do: 80

  @impl true
  def parse(info), do: info
end

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

  • Use Process.monitor to receive messages about the state of a process

    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)
  • Module names are atoms so you can store them in variables

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)

Components

Create components with functions

def button2(assigns) do
  ~H"""
  <button>
    {@text}
  </button>
  """
end

Use them with:

<.button text="hey"/>

Contexts

  • Use to model interactions with the db

Plugs

  • Plugs get executed on every render

Mount/3

  • gets executed on every render

Sessions

  • essentially a genserver per user

Conn

  • connections are ephemeral state stored per request

GenServers

:reply - can be sent by calls to give them a return value

defmodule KeyValueStore do
  use GenServer
  def init(_) do
    {:ok, %{}}
  end

  @impl GenServer
  def handle_cast({:put, key, value}, state) do
    {:noreply, Map.put(state, key, value)}
  end

  def handle_call({:get, key}, _, state) do
    {:reply, Map.get(state, key), state}
  end
end

# Using the genserver
# Start the server
{:ok, pid} = GenServer.start_link(KeyValueStore, _)

GenServer.cast(pid, {:put, :name, "Lance"})
#=> :ok

GenServer.call(pid, {:get, Lance})

## Or register a name
{:ok, _} = GenServer.start_link(Stack, "hello", name: MyStack)

GenServer.call(MyStack, :pop)

OTP Processes

  • Use Task for one-off processes
  • Agent if it's just a data structure

Cast vs. Call

  • Casts are asynchronous, messages to them get queued up
  • calls are synchronous - the caller waits for a response
    • Calls have a default timeout of 5 seconds
    • When a call times out, it remains in the mailbox
    • Note that calls block casts and other non-blocking operations

Continues

You can split a blocking operation (init, call) into blocking and non-blocking segments by having init/1 return {:ok, initial_state, {:continue, some_arg}}

Then implement:

def handle_continue(:init, {name, nil}) do
  val = # ...
{:noreply, {name, val}}
end

Use Case of GenServers

  • Managing long-living state
  • A critical segment of the code needs to be synchronized

Ecto

  • Repos are the abstraction for databases
  • Schemas represent the db structure
    • schemas create struct
  • Changesets - migrations

Queries

:distinct :where :orderby :offset :limit :lock :groupby :having :join :select :preload

Associations

defmodule Post do
  use Ecto.Schema

  schema "posts" do
    has_many :comments, Comment
  end
end

defmodule Comment do
  use Ecto.Schema

  schema "comments" do
    field :title, :string
    belongs_to :post, Post
  end
end

Repo.all from p in Post, preload: [:comments]

Changesets

  • Changesets provide validation rules for updating an entry

  • Changesets are essentially a builder for queries. You pass attributes to them and they go through each of the validation steps then you can pipe it into Repo.insert() and it will fail if it's invalid

  • You can pipe changesets into other changesets to add additional rules. The common pattern is to have in your model something like User.email_changeset that provides validation just for the emails. Then your context would execute the query, e.g.:

    def add_email(attrs) do
      User{}
      |> User.email_changeset(attrs)
      |> # additional validation
      |> Repo.insert()
    defmodule User do
      use Ecto.Schema
      import Ecto.Changeset
    
      schema "users" do
        field :name
        field :email
        field :age, :integer
      end
    
      def changeset(user, params \\ %{}) do
        user
        |> cast(params, [:name, :email, :age]) # the fields allowed to be updated
        |> validate_required([:name, :email]) # these fields must always be non nil
        |> validate_format(:email, ~r/@/)
        |> validate_inclusion(:age, 18..100)
        |> unique_constraint(:email)
      end
    end

Contexts

When you have a file users/user.ex you should have a file up a dir users.ex that includes all of the interactions with User schemas

mix phx.gen.context Things Thing thing name:string description:text

Scopes

Scopes let you build queries step by step

# scope.ex file
defmodule App.Scope do
  import Ecto.Query
  alias App.Blog.Post

  def published(query \\ Post) do
    from p in query, where: p.published == true
  end

  def by_author(query \\ Post, author_id) do
    from p in query, where: p.author_id == ^author_id
  end

  def with_comments(query \\ Post) do
    from p in query, preload: [:comments]
  end

  def created_after(query \\ Post, date) do
    from p in query, where: p.inserted_at > ^date
  end
end

Usage:

Post
|> Scope.published()
|> Scope.by_author(author)
|> Scope.with_comments()
|> Repo.all()

Error Handling

  • BEAM has three types of errors: errors, exits, and throws
  • Errors are generally meant to be thrown, not caught. The process should just restart in these cases

Raises

  • use raise, postfix the function with !

Exits

  • Terminate the process

    exit("Reason")

Throws

  • can be caught with try-catch

Linked Processes

  • If an error happens in the linked process, it's link crashes too
  • Start with spawn_link

Exit Traps

Prevent exit signals

spawn(fn ->
  Process.flag(:trap_exit, true)
)

**