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)
(list) # head of the list
hd(list) # the rest of the list
tl
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
(tup, 1) # first element
elem
= put_elem(tup, 1, 26) # update index 1 to 26 tup
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
= %{:a => 1, :b => 2}
map
# This is equivalent to:
= %{a => 1, b => 2}
map
[:a]
map
# or
Map.get(map, :a, default)
#or
.a # works for atom keys
map
Map.fetch(:a)
# returns {:ok, 1} or :error
Map.put(map, :c, 3)
# Updating a field
= %{map | c: 10} map
MapSets
Use as your default set implementation
MapSet.new([:monday, :tuesday, :wednesday])
MapSet.member?(days, :monday)
# true
Datetimes
= ~D[2023-01-31]
date .year date
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
= [0 | list]
list
# matching maps
{name: name, age: age} = bob
%
# matching binaries
<<b1, b2, b3>> = "ABC"
<<b1, rest :: binary>> = binary
# chaining matches
= (b = 1 + 3) # parens are optional
a # a = 4
Pin Operator
Does a match without assignment
= 1
x ^x = 2 # no match
Case
= 4
x 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
= fn
test_num when is_number(x) and x < 0 -> :negative
x when is_number(x) and x > 0 -> :positive
x 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}
{:ok, login} <- get_login(),
with {:ok, email} <- get_email(),
{:ok, password} <- get_password(),
{:ok, %{login: login, email: email, password: password}}
Functions
Anonymous Functions
= fn a, b -> a + b end
add .(1, 2) add
Capture Operator
&
captures functions. &1
references the first parameter
= &(&1 + 2)
fun
= &(&1 + &2) # 2-arity function
fun
# 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
(tail, head + accumulator)
sum_listend
# pattern match the base case
def sum_list([], accumulator) do
accumulatorend
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,
<- 1..9,
y <= y, # filter
x 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 resend 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
= spawn(fn -> ...)
pid # create a process, this returns the PID
(pid, variable)
send
# on the receiver
receive do
-> func()
pattern_1 -> func2()
pattern_2 after
5000 -> IO.puts("no message found after 5000 secs")
end
= self() # get the current process's PID
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 (&loop/0) spawnend defp loop do receive do ... end () # tail recurse to loop loopend end
Stateful Processes
def start do
(fn ->
spawn= ...
initial_state (initial_state)
loopend)
end
defp loop(state) do
...
(state)
loopend
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
= 1..2
range 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:
("/thermostat/:house") live
Templates
<section :for={post <- @posts} :key={post.id}>
<h1>{expand_title(post.title)}</h1>
</section>
Reactive variables:
(assigns, sum: assigns.x + assigns.y) assign