Run this notebook

Use Livebook to open this notebook and explore new ideas.

It is easy to get started, on your machine or the cloud.

Click below to open and run it in your Livebook at .

(or change your Livebook location)

# Compose in Context with Monads ```elixir Mix.install([ {:fun_park, git: "https://github.com/JKWA/funpark_notebooks.git", branch: "main" } ]) ``` ## Advanced Functional Programming with Elixir | | | | -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | <img src="https://www.joekoski.com/assets/images/jkelixir_small.jpg" alt="Book cover" width="120"> | **Interactive Examples from Chapter 6**<br/>[Advanced Functional Programming with Elixir](https://pragprog.com/titles/jkelixir/advanced-functional-programming-with-elixir). | ## Build the Monad ### Transform with a Functor ````markdown ```elixir def promotion(%__MODULE__{} = patron, points) do change(patron, %{reward_points: patron.reward_points + points}) end ``` ```` Let's start by generating a list of patrons: ```elixir alice = FunPark.Patron.make("Alice", 14, 125, reward_points: 25) beth = FunPark.Patron.make("Beth", 15, 140, reward_points: 10) charles = FunPark.Patron.make("Charles", 13, 130, reward_points: 50) patrons = [alice, beth, charles] ``` With Elixir's `Enum.map/2` functor, we can apply `promotion/2` to each patron, adding 10 to their reward points: ```elixir patrons |> Enum.map(&FunPark.Patron.promotion(&1, 10)) ``` ### Sequence Computations First, we need to define a Kleisli function: ```elixir kleisli_fn = fn x -> if rem(x, 2) == 0, do: [x * x], else: [] end ``` Next, we need a list of values: ```elixir list = [1, 2, 3, 4, 5, 6] ``` When we apply our Kleisli function: ```elixir list |> Enum.flat_map(kleisli_fn) ``` ### Independent Computations ```elixir ap = fn values, funcs -> for f <- funcs, v <- values, do: f.(v) end ``` Next, we need a couple of simple functions, `add_one/1` and `add_two/1`: ```elixir add_one = fn x -> x + 1 end add_two = fn x -> x + 2 end func_list = [add_one, add_two] ``` And we need a list of values: ```elixir list = [10, 20, 30] ``` Finally, we use `ap` to apply our list of functions to our list of values: ```elixir list |> ap.(func_list) ``` ### The Protocol ````markdown ```elixir defprotocol FunPark.Monad do def map(monad, function) def bind(monad, function) def ap(monad, function) end ``` ```` ## Model Neutrality with Identity ````markdown ```elixir defmodule FunPark.Identity do defstruct value: nil def pure(value), do: %__MODULE__{value: value} def extract(%__MODULE__{value: value}), do: value end ``` ```` Let's start by generating a patron: ```elixir alice = FunPark.Patron.make("Alice", 14, 130) ``` We can lift Alice into the `Identity` context: ```elixir alice_monad = FunPark.Identity.pure(alice) ``` And we can extract her: ```elixir FunPark.Identity.extract(alice_monad) ``` In fact, we can pass anything through the `Identity` monad with no effect: ```elixir :apple |> FunPark.Identity.pure() |> FunPark.Identity.extract() ``` ### Equality ````markdown ```elixir defimpl FunPark.Eq, for: FunPark.Identity do alias FunPark.Eq alias FunPark.Identity 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 ``` ```` Let's start by generating a couple of patrons: ```elixir alice = FunPark.Patron.make("Alice", 14, 130) beth = FunPark.Patron.make("Beth", 16, 125) ``` The `Eq` protocol knows when they are equivalent: ```elixir FunPark.Eq.Utils.eq?(alice, alice) ``` ```elixir FunPark.Eq.Utils.eq?(alice, beth) ``` Even when they're wrapped in the `Identity` monad: ```elixir alice_monad = FunPark.Identity.pure(alice) beth_monad = FunPark.Identity.pure(beth) FunPark.Eq.Utils.eq?(alice_monad, alice_monad) ``` ```elixir FunPark.Eq.Utils.eq?(alice_monad, beth_monad) ``` ### Ordering ````markdown ```elixir 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, let's generate our `Patrons`: ```elixir alice = FunPark.Patron.make("Alice", 14, 135, ticket_tier: :vip) beth = FunPark.Patron.make("Beth", 16, 125) ``` The `Patron` context's default ordering is by name, so Alice is less than Beth: ```elixir FunPark.Ord.Utils.compare(alice, beth) ``` It makes no difference if they are wrapped in `Identity` monads: ```elixir alice_monad = FunPark.Identity.pure(alice) beth_monad = FunPark.Identity.pure(beth) FunPark.Ord.Utils.compare(alice_monad, beth_monad) ``` ### Lift Eq and Order ````markdown ```elixir def lift_eq(custom_eq) do custom_eq = Eq.Utils.to_eq_map(custom_eq) %{ eq?: fn %__MODULE__{value: v1}, %__MODULE__{value: v2} -> custom_eq.eq?.(v1, v2) end, not_eq?: fn %__MODULE__{value: v1}, %__MODULE__{value: v2} -> custom_eq.not_eq?.(v1, v2) end } end ``` ```` ````markdown ```elixir 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 ``` ```` Let's return to our patrons: ```elixir alice = FunPark.Patron.make("Alice", 14, 135, ticket_tier: :vip) beth = FunPark.Patron.make("Beth", 16, 125) ``` We have custom ordering based on priority: ```elixir priority_ord = FunPark.Patron.ord_by_priority() ``` Within the context of priority, Alice—with her VIP status—is greater than Beth: ```elixir FunPark.Ord.Utils.compare(alice, beth, priority_ord) ``` We can lift that `priority_ord` into the Identity context: ```elixir lifted_priority_ord = FunPark.Identity.lift_ord(priority_ord) ``` And we get the same result for our wrapped patrons: ```elixir alice_monad = FunPark.Identity.pure(alice) beth_monad = FunPark.Identity.pure(beth) FunPark.Ord.Utils.compare(alice_monad, beth_monad, lifted_priority_ord) ``` ### Monadic Logic ````markdown ```elixir defimpl FunPark.Monad, for: FunPark.Identity do def map(%FunPark.Identity{value: value}, function) do %FunPark.Identity{value: function.(value)} end def bind(%FunPark.Identity{value: value}, function) do function.(value) end def ap(%FunPark.Identity{value: function}, %FunPark.Identity{value: value}) do %FunPark.Identity{value: function.(value)} end end ``` ```` ````markdown ```elixir def add_wait_time(%__MODULE__{} = ride, minutes) when minutes >= 0 do change(ride, %{wait_time: ride.wait_time + minutes}) end ``` ```` Let's generate the Tea Cup ride: ```elixir tea_cup = FunPark.Ride.make("Tea Cup", wait_time: 10) ``` We can add 20 minutes: ```elixir FunPark.Ride.add_wait_time(tea_cup, 20) ``` And because `add_wait_time/2` is closed under its operation, we can use the pipe operator to compose multiple sensors: ```elixir tea_cup |> FunPark.Ride.add_wait_time(20) |> FunPark.Ride.add_wait_time(10) |> FunPark.Ride.add_wait_time(5) ``` The same applies within the `Identity` context. First, let's lift the Tea Cup ride: ```elixir tea_cup_m = FunPark.Identity.pure(tea_cup) ``` To make things easier, we'll curry the `add_wait_time/2` function: ```elixir add_wait = FunPark.Utils.curry_r(&FunPark.Ride.add_wait_time/2) ``` Now we can then apply it to the monad using `map/2`: ```elixir FunPark.Monad.map(tea_cup_m, add_wait.(20)) ``` And again, we can compose updates from three sensors: ```elixir tea_cup_m |> FunPark.Monad.map(add_wait.(20)) |> FunPark.Monad.map(add_wait.(10)) |> FunPark.Monad.map(add_wait.(5)) ``` Even so, we can model our sensors so they choose the structure: ```elixir sensor_1 = &FunPark.Identity.pure(add_wait.(10).(&1)) sensor_2 = &FunPark.Identity.pure(add_wait.(5).(&1)) sensor_3 = &FunPark.Identity.pure(add_wait.(20).(&1)) ``` We chain Kleisli functions with `bind/2`: ```elixir tea_cup_m |> FunPark.Monad.bind(sensor_1) |> FunPark.Monad.bind(sensor_2) |> FunPark.Monad.bind(sensor_3) ```
See source

Have you already installed Livebook?

If you already installed Livebook, you can configure the default Livebook location where you want to open notebooks.
Livebook up Checking status We can't reach this Livebook (but we saved your preference anyway)
Run notebook

Not yet? Install Livebook in just a minute

Livebook is open source, free, and ready to run anywhere.

Run on your machine

with Livebook Desktop

Run in the cloud

on select platforms

To run on Linux, Docker, embedded devices, or Elixir’s Mix, check our README.

PLATINUM SPONSORS
SPONSORS
Code navigation with go to definition of modules and functions Read More ×