Elixir
Strings
Concatenation
"hello " <> "world"Interpolation
"hello #{world}"
"5 + 10 = #{5 + 10}"Sigils
~s("Includes quotes")
~S("raw backslash char: \") # doesn't escapeCharlists
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} # tupleAtoms
- Atoms are never garbage collected
Functions
oneline functions
def area(a, b) do: a * bDefault Values
def area(a, b \\ 0) do: a * bLogic
true and true
true or trueData 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 keysThese 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 26Modifying 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)
# trueDatetimes
date = ~D[2023-01-31]
date.yearPattern 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 = 4Pin Operator
Does a match without assignment
x = 1
^x = 2 # no matchCase
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"
endErrors 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"
endGuards
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
endGuards 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
endwith
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
endType Hints
@spec area(number) :: number
def area(r) do: r * r * @piBinaries, 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 fieldPolymorphism
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
endBEAM
- 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)
endManaging 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>
  """
endUse 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 Taskfor one-off processes
- Agentif 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}}
endUse 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_changesetthat 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:textScopes
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
endUsage:
Post
|> Scope.published()
|> Scope.by_author(author)
|> Scope.with_comments()
|> Repo.all()Error Handling
- BEAM has three types of errors: errors,exits, andthrows
- 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)
)**