If you can't read please download the document
Upload
tomasz-kowal
View
238
Download
2
Embed Size (px)
Citation preview
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 = 1two = 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 -> endend
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 endend
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 endend
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 endend
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 applications2. 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 connend
Pipeline
Pipeline is a set of plugspipeline :pipeline_name do plug :plug1 plug :plug2end
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 :controllerend
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) endend
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) endend
current_user test
defmodule Fakerepo do def get(1) do %Account{name: Tomasz, } endend
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.
ccserver
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
updateviewnew_model
commands
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
GenServers 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