Very basic functional design patterns

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