Functional Design Patterns
Tomasz Kowal
Why this talk?
Why this talk?
Syntax
Pattern matching
and pin operator
Basic recursion, map,
reduce, tail recursion
Concurrency,
distributed systems
Why this talk?
Syntax
Pattern matching
and pin operator
Basic recursion, map,
reduce, tail recursion
Concurrency,
distributed systems
Why this talk?
Syntax
Pattern matching
and pin operator
Basic recursion, map,
reduce, tail recursion
How do I build stuff?
Questions
Please ask during the talk!
1 + 1
Patterns
1. Use single data structure that is your single
source of truth and many small functions
operating on it.
2. Separate pure and impure parts.
Pipe operator
two = double(1)
two =
1
|> double()
Pipe operator
five = add(2, 3)
five =
2
|> add(3)
Pipe operator
one = 1
two = double(one)
four = double(two)
1
|> double()
|> double()
Pipe operator
list = [1, 10, 3, 12, 42]
Pipe operator
list = [1, 10, 3, 12, 42]
filtered = Enum.filter(
list,
&Integer.is_even/1
)
Pipe operator
list = [1, 10, 3, 12, 42]
filtered = Enum.filter(
list,
&Integer.is_even/1
)
sorted = Enum.sort(filtered)
Pipe operator
sorted =
list
|> Enum.filter(&Integer.is_even/1)
|> Enum.sort()
Pipe operator
sorted =
list
|> Enum.filter(&Integer.is_even/1)
|> Enum.sort()
sorted = Enum.sort(Enum.filter(list,
&Integer.is_even/1))
Pipe operator
def sorted_evens(list) do
list
|> Enum.filter(&Integer.is_even/1)
|> Enum.sort()
end
Pipe operator
def two_smallest_evens(list) do
list
|> sorted_evens()
|> Enum.take(2)
end
list list list
transform
list list list
transform
„Debugging” with IO.inspect
def two_smallest_evens(list) do
list
|> sorted_evens()
|> IO.inspect()
|> Enum.take(2)
end
Data validation problem
● Current value:
%User{name: „Tom”, age: 29}
● Things that we want to change:
%{age: 30}
● Is the data valid? If not – why?
Ecto.Changeset fields
● valid?
● data
● params
● changes
● errors
● ...
Validating with Ecto.Changeset
user
|> cast(params, [:name, :email, :age])
user changeset
cast
Validating with Ecto.Changeset
user
|> cast(params, [:name, :email, :age])
|> validate_required([:name, :email])
user changeset changeset
cast validate1
Validating with Ecto.Changeset
user
|> cast(params, [:name, :email, :age])
|> validate_required([:name, :email])
|> validate_format(:email, ~r/@/)
user changeset changeset changeset
cast validate1 validate2
Custom validator
● Lets say we have an event with start
date and end date.
● We want to make sure that start date is
not after end date.
Custom validator
def validate_interval(changeset, start, end)
do
end
Custom validator
def validate_interval(changeset, start, end) do
start_date = get_field(changeset, start)
end_date = get_field(changeset, end)
end
get_field
● Looks for value in params
● Looks for value in original data
Custom validator
def validate_interval(changeset, start, end) do
start_date = get_field(changeset, start)
end_date = get_field(changeset, end)
case Date.compare(start_date, end_date) do
:gt ->
_otherwise ->
end
end
Custom validator
def validate_interval(changeset, start, end) do
start_date = get_field(changeset, start)
end_date = get_field(changeset, end)
case Date.compare(start_date, end_date) do
:gt ->
_otherwise -> changeset
end
end
Custom validator
def validate_interval(changeset, start, end) do
start_date = get_field(changeset, start)
end_date = get_field(changeset, end)
case Date.compare(start_date, end_date) do
:gt -> add_error(changeset, start, "…")
_otherwise -> changeset
end
end
add_error
● appends new error to list of errors
● changes valid? to false
● takes changeset and returns changeset
Custom validator
def validate_interval(changeset, start, end) do
start_date = get_field(changeset, start)
end_date = get_field(changeset, end)
case Date.compare(start_date, end_date) do
:gt -> add_error(changeset, start, "…")
_otherwise -> changeset
end
end
Composing validators
def validate_address(cs, street, zip) do
cs
|> validate_street(street)
|> validate_zipcode(zip)
end
changeset changeset changeset
validate1
cs cs cs
v1
validate2
v2
Inserting to database
case Repo.insert(changeset) do
{:error, changeset} →...
{:ok, model} →...
end
Benefits
● Easy to compose: just |> another validator
● Easy to extend with custom validators
● Easy to test: all functions are pure
● Bonus: it works for any data.
Immutability digression
● Creating new structure every time is optimized
when language provides immutability
● list = [1, 2, 3]
1 2 3
Immutability digression
● Creating new structure every time is optimized
when language provides immutability
● list = [1, 2, 3]
● list2 = [0 | list]
0
1 2 3
Immutability digression
● Creating new structure every time is optimized
when language provides immutability
● list = [1, 2, 3]
● list2 = [0 | list]
● list3 = [4 | list]
0
4
1 2 3
Using Ecto.Multi
Multi.new
|> Multi.update(:account, a_changeset))
|> Multi.insert(:log, log_changeset))
|> Multi.delete_all(:sessions,
assoc(account, :sessions))
multi |> Repo.transaction
Composition
def extend_multi(multi, changeset) do
multi
|> Multi.insert(:some_tag, changeset)
end
multi multi multi
custom
multi multi multi
ins
insert
upd
How would you test this?
Repo.update(...)
Repo.insert(...)
Repo.delete_all(...)
Unit testing Ecto.Multi
assert [
{:account, {:update, achangeset, []}},
{:log, {:insert, log_changeset, []}},
{:sessions, {:delete_all, query, []}}
] = Ecto.Multi.to_list(multi)
Benefits
● Easy to compose: just |> another operation
● Easy to extend with Multi.run
● Easy to test with Multi.to_list
Garbage Collection digression
● Erlang GC is run separately for all processes
● When process dies, all its memory is freed
● This means that, if
– you use a process per request in your web
application and
– it has short lifecycle
The garbage collection may never happen!
Plug (or what makes Phoenix cool)
1. A specification for composable modules
between web applications
2. Connection adapters for different web
servers in the Erlang VM
Plug.Conn
● host
● method
● path_info
● req_headers
● params
● assigns
● resp_body
● status
A plug
def do_nothing(conn) do
conn
end
Pipeline
● Pipeline is a set of plugs
pipeline :pipeline_name do
plug :plug1
plug :plug2
end
● Pipeline is a plug
conn conn conn
pipeline
conn conn conn
v1
plug
v2
Almost the same...
def pipeline_name(conn) do
conn
|> if_not_halted(plug1)
|> if_not_halted(plug2)
end
The glue
def if_not_halted(conn, plug) do
if conn.halted? do
conn
else
plug(conn)
end
Phoenix is a pipeline
pipeline :phoenix do
plug :endpoint
plug :user_pipelines
plug :router
plug :controller
end
And lets you add custom plugs
def current_user(conn) do
account_id = get_session(conn, :account_id)
cond do
account = conn.assigns[:current_account] ->
conn
account = account_id && Repo.get(MyApp.Account, account_id) ->
assign(conn, :current_account, account)
true ->
assign(conn, :current_account, nil)
end
end
And lets you add custom plugs
def current_user(conn, repo  Repo) do
account_id = get_session(conn, :account_id)
cond do
account = conn.assigns[:current_account] ->
conn
account = account_id && repo.get(MyApp.Account, account_id) ->
assign(conn, :current_account, account)
true ->
assign(conn, :current_account, nil)
end
end
current_user test
defmodule Fakerepo do
def get(1) do
%Account{name: „Tomasz”, …}
end
end
current_user(conn, Fakerepo)
Benefits
● Easy to compose: set of plugs is a plug
● Easy to extend: your own plugs can be put
anywhere in the request cycle
● Easy to test: but you need to ensure explicit
contracts
Naming digression
● burritos
Naming digression
● burritos
● >>=
Naming digression
● burritos
● >>=
● Maybe, IO
Naming digression
● burritos
● >>=
● Maybe, IO
● A monad is just a monoid in the category of
endofunctors
GenServer
● GenServer abstracts an actor that takes
requests from the outside world and keeps
state.
c
c
server
c call
cast
cast
GenServer
def handle_call(msg, from, state) do
...
{:reply, actual_reply, new_state}
end
Benefits
● Easy to compose: not covered
● Easy to extend: just add another handle_call
● Easy to test: it is not even necessary to start it!
Elm Architecture
update
view
new_modelcommands
virtual dom
Browser with Elm
msgs model
Benefits
● Easy to compose: recursively
● Easy to extend: just add another message
● Easy to test: only pure functions!
Carrots
Did you know carrots are good for your
eyesight?
Single source of truth
● Changeset
● Multi
● Plug.Conn
● GenServer’s State
● Elm Model
Separate pure and impure
● Keep impure parts separate from core logic
● Make impure parts as function inputs (explict
contracts)
Questions
If you like this talk, follow me on Twitter
@snajper47

Very basic functional design patterns