We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
6. Compose in Context with Monads
Monads are inspired by category theory; monads compose computations within a context. Compose means building a concrete thing from reusable components. Context refers to the surrounding environment: database connection, current user, or a config map.
In functional programming compose means combining behavior. It can involve: read-only (Reader), absence (Maybe), failure (Either), or asynchronous computation (Effect).
Build the Monad
There are 2 operations that comprise a Monad: transformation (Map), and chains of context-aware computations (Bind).
Transform With a Functor
Functors allow follow 2 rules:
• Identity: Mapping with the identity function returns a copy of the original
structure. map(fn x -> x end, F(a)) = F(a)
• Composition: Mapping in two steps is the same as mapping once with a
composed function. map(f, map(g, F(a))) = map(fn x -> f.(g.(x)) end, F(a))
We want to add a new function that will add points to a Patrons reward_points.
def promotion(%__MODULE__{} = patron, points) do
new_points = Math.sum(get_reward_points(patron), points)
change(patron, %{reward_points: new_points})
end
Run It
iex(1)> alice = FunPark.Patron.make("Alice", 14, 125, reward_points: 25)
%FunPark.Patron{
id: 3138,
name: "Alice",
age: 14,
height: 125,
ticket_tier: :basic,
fast_passes: [],
reward_points: 25,
likes: [],
dislikes: []
}
iex(2)> beth = FunPark.Patron.make("Beth", 15, 140, reward_points: 10)
%FunPark.Patron{
id: 3202,
name: "Beth",
age: 15,
height: 140,
ticket_tier: :basic,
fast_passes: [],
reward_points: 10,
likes: [],
dislikes: []
}
iex(3)> charles = FunPark.Patron.make("Charles", 13, 130, reward_points: 50)
%FunPark.Patron{
id: 3266,
name: "Charles",
age: 13,
height: 130,
ticket_tier: :basic,
fast_passes: [],
reward_points: 50,
likes: [],
dislikes: []
}
iex(4)> patrons = [alice, beth, charles]
[
%FunPark.Patron{
id: 3138,
name: "Alice",
age: 14,
height: 125,
ticket_tier: :basic,
fast_passes: [],
reward_points: 25,
likes: [],
dislikes: []
},
%FunPark.Patron{
id: 3202,
name: "Beth",
age: 15,
height: 140,
ticket_tier: :basic,
fast_passes: [],
reward_points: 10,
likes: [],
dislikes: []
},
%FunPark.Patron{
id: 3266,
name: "Charles",
age: 13,
height: 130,
ticket_tier: :basic,
fast_passes: [],
reward_points: 50,
likes: [],
dislikes: []
}
]
iex(5)> patrons |> Enum.map(&FunPark.Patron.promotion(&1, 10))
[
%FunPark.Patron{
id: 3138,
name: "Alice",
age: 14,
height: 125,
ticket_tier: :basic,
fast_passes: [],
reward_points: 35,
likes: [],
dislikes: []
},
%FunPark.Patron{
id: 3202,
name: "Beth",
age: 15,
height: 140,
ticket_tier: :basic,
fast_passes: [],
reward_points: 20,
likes: [],
dislikes: []
},
%FunPark.Patron{
id: 3266,
name: "Charles",
age: 13,
height: 130,
ticket_tier: :basic,
fast_passes: [],
reward_points: 60,
likes: [],
dislikes: []
}
]
Sequence Computations
A monad includes behavior for chaining computations within a context. For the remainder, as there is no agreed upon name, we will call it bind.
Bind operators follow three rules:
The bind operation follows three laws:
• Left identity: Wrapping a value and then binding it to a function is the
same as applying the function directly. bind(pure(a), f) = f(a)
• Right identity: Binding a monad to pure has no effect. bind(m, pure) = m
• Associativity: It doesn’t matter how you nest your bindings—the result is
the same. bind(bind(m, f), g) = bind(m, fn x -> bind(f(x), g) end)
Run It
First let’s bind the Kleisli function that takes an input and returns a function.
iex(6)> kleisli_fn = fn x -> if rem(x, 2) == 0, do: [x * x], else: [] end
#Function<42.39164016/1 in :erl_eval.expr/6>
iex(7)> list = [1, 2, 3, 4, 5, 6]
[1, 2, 3, 4, 5, 6]
iex(8)> list |> Enum.flat_map(kleisli_fn)
[4, 16, 36]
Independent Computations
Applicative is useful when we need to combine two things that are already inside a context. It follows four fundamental rules:
• Identity: Applying a wrapped identity function has no effect. ap(pure(fn x ->
x end), F(a)) = F(a)
• Homomorphism: Lifting a function and a value separately is the same as
applying them directly. ap(pure(f), pure(a)) = pure(f.(a))
• Interchange: A function in context can be applied to a pure value, or the
value can be lifted into a function and applied to the context instead.
ap(F(f), pure(a)) = ap(pure(fn g -> g.(a) end), F(f))
• Composition: Applying functions step by step inside the context behaves
the same as applying them all at once. ap(ap(ap(pure(fn f -> fn g -> fn x -> f.(g.(x)))
end, F(f)), F(g)), F(a)) = ap(F(f), ap(F(g), F(a)))
Before we run it it seems that the next bit of code will do 2 things to a list and then combine the outcomes into 1 longer list.
Run It
iex(9)> ap = fn values, funcs -> for f <- funcs, v <- values, do: f.(v) end
#Function<41.39164016/2 in :erl_eval.expr/6>
iex(10)> add_one = fn x -> x + 1 end
#Function<42.39164016/1 in :erl_eval.expr/6>
iex(11)> add_two = fn x -> x + 2 end
#Function<42.39164016/1 in :erl_eval.expr/6>
iex(12)> func_list = [add_one, add_two]
[#Function<42.39164016/1 in :erl_eval.expr/6>,
#Function<42.39164016/1 in :erl_eval.expr/6>]
iex(13)> list = [10, 20, 30]
[10, 20, 30]
iex(14)> list |> ap.(func_list)
[11, 21, 31, 12, 22, 32]
Bind each step depends on the results of the previous, ap happens independently.
The Protocol
defprotocol FunPark.Monad do
def map(monad_value, func)
def bind(monad_value, func_returning_monad)
def ap(monadic_func, monad_value)
end
1. map/2 applies a function to a value in a context, preserving the structure.
2. bind/2 sequences computations, allowing each step to determine the next,
all within the context.
3. ap/2 applies a function to a value, where both are in the same context.
Model Neutrality with Identity
Okay let’s start out with the identity module.
@enforce_keys [:value]
defstruct [:value]
def pure(value), do: %__MODULE__{value: value}
def extract(%__MODULE__{value: value}), do: value
Pure is the identity and extract/1 is used to access the value.
Run It
iex(15)> alice = FunPark.Patron.make("Alice", 14, 130)
%FunPark.Patron{
id: 3714,
name: "Alice",
age: 14,
height: 130,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(16)> alice_monad = FunPark.Identity.pure(alice)
%FunPark.Identity{
value: %FunPark.Patron{
id: 3714,
name: "Alice",
age: 14,
height: 130,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
}
iex(17)> FunPark.Identity.extract(alice_monad)
%FunPark.Patron{
id: 3714,
name: "Alice",
age: 14,
height: 130,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(18)> :apple |> FunPark.Identity.pure() |> FunPark.Identity.extract()
:apple
This is just the beginning but we are simply adding the value passed into the struct for Identity and then accessing it.
Equality
defimpl FunPark.Eq, for: FunPark.Identity do
alias FunPark.Identity
alias FunPark.Eq
def eq?(%Identity{value: v1}, %Identity{value: v2}), do: Eq.eq?(v1, v2)
def not_eq?(%Identity{value: v1}, %Identity{value: v2}),
do: Eq.not_eq?(v1, v2)
end
This will unwrap and then apply the EQ protocol.
Run It
iex(23)> FunPark.Eq.Utils.eq?(alice, alice)
true
iex(24)> FunPark.Eq.Utils.eq?(alice, beth)
false
iex(25)> alice_monad = FunPark.Identity.pure(alice)
%FunPark.Identity{
value: %FunPark.Patron{
id: 4034,
name: "Alice",
age: 14,
height: 130,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
}
iex(26)> beth_monad = FunPark.Identity.pure(beth)
%FunPark.Identity{
value: %FunPark.Patron{
id: 4098,
name: "Beth",
age: 16,
height: 125,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
}
iex(27)> FunPark.Eq.Utils.eq?(alice_monad, alice_monad)
iex(27)> FunPark.Eq.Utils.eq?(alice_monad, alice_monad)
true
iex(28)> FunPark.Eq.Utils.eq?(alice_monad, beth_monad)
false
You can see that the monad and the pure struct are the same things here.
Ordering
defimpl FunPark.Ord, for: FunPark.Identity do
alias FunPark.Ord
alias FunPark.Identity
def lt?(%Identity{value: v1}, %Identity{value: v2}), do: Ord.lt?(v1, v2)
def le?(%Identity{value: v1}, %Identity{value: v2}), do: Ord.le?(v1, v2)
def gt?(%Identity{value: v1}, %Identity{value: v2}), do: Ord.gt?(v1, v2)
def ge?(%Identity{value: v1}, %Identity{value: v2}), do: Ord.ge?(v1, v2)
end
Again an unwrap and then a comparison
Run It
iex(29)> alice = FunPark.Patron.make("Alice", 14, 135, ticket_tier: :vip)
%FunPark.Patron{
id: 4162,
name: "Alice",
age: 14,
height: 135,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(30)> beth = FunPark.Patron.make("Beth", 16, 125)
%FunPark.Patron{
id: 4226,
name: "Beth",
age: 16,
height: 125,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(31)> FunPark.Ord.Utils.compare(alice, beth)
:lt
iex(32)> alice_monad = FunPark.Identity.pure(alice)
%FunPark.Identity{
value: %FunPark.Patron{
id: 4162,
name: "Alice",
age: 14,
height: 135,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
}
iex(33)> beth_monad = FunPark.Identity.pure(beth)
%FunPark.Identity{
value: %FunPark.Patron{
id: 4226,
name: "Beth",
age: 16,
height: 125,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
}
iex(34)> FunPark.Ord.Utils.compare(alice_monad, beth_monad)
:lt
Lift Eq and Order
We can now use some custom Eq and Ord implementations.
def lift_eq(custom_eq) do
custom_eq = Eq.Utils.to_eq_map(custom_eq)
%{
eq?: fn
%__MODULE__{value: a}, %__MODULE__{value: b} -> custom_eq.eq?.(a, b)
end,
not_eq?: fn
%__MODULE__{value: a}, %__MODULE__{value: b} ->
custom_eq.not_eq?.(a, b)
end
}
end
def lift_ord(custom_ord) do
custom_ord = Ord.Utils.to_ord_map(custom_ord)
%{
lt?: fn
%__MODULE__{value: v1}, %__MODULE__{value: v2} ->
custom_ord.lt?.(v1, v2)
end,
le?: fn
%__MODULE__{value: v1}, %__MODULE__{value: v2} ->
custom_ord.le?.(v1, v2)
end,
gt?: fn
%__MODULE__{value: v1}, %__MODULE__{value: v2} ->
custom_ord.gt?.(v1, v2)
end,
ge?: fn
%__MODULE__{value: v1}, %__MODULE__{value: v2} ->
custom_ord.ge?.(v1, v2)
end
}
end
These will unwrap the Eq and Ord functions as well as the values from the Monad.
Run It
iex(35)> alice = FunPark.Patron.make("Alice", 14, 135, ticket_tier: :vip)
%FunPark.Patron{
id: 4290,
name: "Alice",
age: 14,
height: 135,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(36)> beth = FunPark.Patron.make("Beth", 16, 125)
%FunPark.Patron{
id: 4354,
name: "Beth",
age: 16,
height: 125,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(37)> priority_ord = FunPark.Patron.ord_by_priority()
%{
ge?: #Function<3.54941642/2 in FunPark.Monoid.FunPark.Monoid.Ord.append/2>,
gt?: #Function<2.54941642/2 in FunPark.Monoid.FunPark.Monoid.Ord.append/2>,
le?: #Function<1.54941642/2 in FunPark.Monoid.FunPark.Monoid.Ord.append/2>,
lt?: #Function<0.54941642/2 in FunPark.Monoid.FunPark.Monoid.Ord.append/2>
}
iex(38)> FunPark.Ord.Utils.compare(alice, beth, priority_ord)
:gt
iex(39)> lifted_priority_ord = FunPark.Identity.lift_ord(priority_ord)
%{
ge?: #Function<6.74003289/2 in FunPark.Identity.lift_ord/1>,
gt?: #Function<5.74003289/2 in FunPark.Identity.lift_ord/1>,
le?: #Function<4.74003289/2 in FunPark.Identity.lift_ord/1>,
lt?: #Function<3.74003289/2 in FunPark.Identity.lift_ord/1>
}
iex(40)> alice_monad = FunPark.Identity.pure(alice)
%FunPark.Identity{
value: %FunPark.Patron{
id: 4290,
name: "Alice",
age: 14,
height: 135,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
}
iex(41)> beth_monad = FunPark.Identity.pure(beth)
%FunPark.Identity{
value: %FunPark.Patron{
id: 4354,
name: "Beth",
age: 16,
height: 125,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
}
iex(42)> FunPark.Ord.Utils.compare(alice_monad, beth_monad, lifted_priority_ord)
:gt
Monadic Logic
defimpl FunPark.Monad, for: FunPark.Identity do
alias FunPark.Identity
def map(%Identity{value: value}, func) do
Identity.pure(func.(value))
end
def bind(%Identity{value: value}, func) do
func.(value)
end
def ap(%Identity{value: func}, %Identity{value: value}) do
Identity.pure(func.(value))
end
end
• map/2 unwraps the value from the Identity, applies the function, and rewraps
the result.
• bind/2 unwraps the value from the Identity and applies it to the provided
function, which must return a new Identity.
• ap/2 unwraps both the function and the value, applies the function to the
value, and rewraps the result.
Now let’s take this and doing some work to add some wait times together.
def add_wait_time(
%__MODULE__{wait_time: wait_time} = ride,
minutes
)
when is_number(minutes) and minutes > 0 do
change(ride, %{wait_time: Math.sum(wait_time, minutes)})
end
This will add the wait time to the current wait time ensuring that the new time is positive.
Run It
iex(43)> tea_cup = FunPark.Ride.make("Tea Cup", wait_time: 10)
%FunPark.Ride{
id: 4418,
name: "Tea Cup",
min_age: 0,
min_height: 0,
wait_time: 10,
online: true,
tags: []
}
iex(44)> FunPark.Ride.add_wait_time(tea_cup, 20)
%FunPark.Ride{
id: 4418,
name: "Tea Cup",
min_age: 0,
min_height: 0,
wait_time: 30,
online: true,
tags: []
}
iex(45)> tea_cup
%FunPark.Ride{
id: 4418,
name: "Tea Cup",
min_age: 0,
min_height: 0,
wait_time: 10,
online: true,
tags: []
}
iex(46)> |> FunPark.Ride.add_wait_time(20)
%FunPark.Ride{
id: 4418,
name: "Tea Cup",
min_age: 0,
min_height: 0,
wait_time: 30,
online: true,
tags: []
}
iex(47)> |> FunPark.Ride.add_wait_time(10)
%FunPark.Ride{
id: 4418,
name: "Tea Cup",
min_age: 0,
min_height: 0,
wait_time: 40,
online: true,
tags: []
}
iex(48)> |> FunPark.Ride.add_wait_time(5)
%FunPark.Ride{
id: 4418,
name: "Tea Cup",
min_age: 0,
min_height: 0,
wait_time: 45,
online: true,
tags: []
}
iex(50)> tea_cup_m = FunPark.Identity.pure(tea_cup)
%FunPark.Identity{
value: %FunPark.Ride{
id: 4418,
name: "Tea Cup",
min_age: 0,
min_height: 0,
wait_time: 10,
online: true,
tags: []
}
}
iex(51)> add_wait = FunPark.Utils.curry_r(&FunPark.Ride.add_wait_time/2)
#Function<3.64560675/1 in FunPark.Utils.curry_r/3>
iex(52)> FunPark.Monad.map(tea_cup_m, add_wait.(20))
%FunPark.Identity{
value: %FunPark.Ride{
id: 4418,
name: "Tea Cup",
min_age: 0,
min_height: 0,
wait_time: 30,
online: true,
tags: []
}
}
iex(53)> tea_cup_m
%FunPark.Identity{
value: %FunPark.Ride{
id: 4418,
name: "Tea Cup",
min_age: 0,
min_height: 0,
wait_time: 10,
online: true,
tags: []
}
}
iex(54)> |> FunPark.Monad.map(add_wait.(20))
%FunPark.Identity{
value: %FunPark.Ride{
id: 4418,
name: "Tea Cup",
min_age: 0,
min_height: 0,
wait_time: 30,
online: true,
tags: []
}
}
iex(55)> |> FunPark.Monad.map(add_wait.(10))
%FunPark.Identity{
value: %FunPark.Ride{
id: 4418,
name: "Tea Cup",
min_age: 0,
min_height: 0,
wait_time: 40,
online: true,
tags: []
}
}
iex(56)> |> FunPark.Monad.map(add_wait.(5))
%FunPark.Identity{
value: %FunPark.Ride{
id: 4418,
name: "Tea Cup",
min_age: 0,
min_height: 0,
wait_time: 45,
online: true,
tags: []
}
}
We can see that you can now add wait times and then even pipe them into each other. We also can do the same thing with the Identity of the Monad, using the map. Let’s continue.
The bind/2 function differs in one key way: it lets each step choose the structure of the result. In monads with multiple structures—like Maybe or Either—bind can switch between them. But Identity has only one structure, so there’s nothing to switch to.
iex(57)> sensor_1 = &FunPark.Identity.pure(add_wait.(10).(&1))
#Function<42.39164016/1 in :erl_eval.expr/6>
iex(58)> sensor_2 = &FunPark.Identity.pure(add_wait.(5).(&1))
#Function<42.39164016/1 in :erl_eval.expr/6>
iex(59)> sensor_3 = &FunPark.Identity.pure(add_wait.(20).(&1))
#Function<42.39164016/1 in :erl_eval.expr/6>
iex(60)> tea_cup_m
%FunPark.Identity{
value: %FunPark.Ride{
id: 4418,
name: "Tea Cup",
min_age: 0,
min_height: 0,
wait_time: 10,
online: true,
tags: []
}
}
iex(61)> |> FunPark.Monad.bind(sensor_1)
%FunPark.Identity{
value: %FunPark.Ride{
id: 4418,
name: "Tea Cup",
min_age: 0,
min_height: 0,
wait_time: 20,
online: true,
tags: []
}
}
iex(62)> |> FunPark.Monad.bind(sensor_2)
%FunPark.Identity{
value: %FunPark.Ride{
id: 4418,
name: "Tea Cup",
min_age: 0,
min_height: 0,
wait_time: 25,
online: true,
tags: []
}
}
iex(63)> |> FunPark.Monad.bind(sensor_3)
%FunPark.Identity{
value: %FunPark.Ride{
id: 4418,
name: "Tea Cup",
min_age: 0,
min_height: 0,
wait_time: 45,
online: true,
tags: []
}
}
We are doing everything so far without a real need to use this form factor. But the point of all of this is to give us a way to always use the same data structure to do work on a context. The sensors will come from out side the Ride context and we might not be able to control everything that is sent, but we can at least be sure we can pipe the information from one Monad to an other.
What You’ve Learned
Explained above but this is just the beginning.