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)

# Combine with Monoids ```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 4**<br/>[Advanced Functional Programming with Elixir](https://pragprog.com/titles/jkelixir/advanced-functional-programming-with-elixir). | ## Define the Protocol ````markdown ```elixir defprotocol FunPark.Monoid do def empty(monoid) def append(a, b) def wrap(monoid, value) def unwrap(monoid) end ``` ```` ## Combine Numbers with Sum ````markdown ```elixir defmodule FunPark.Monoid.Sum do defstruct value: 0 end ``` ```` ````markdown ```elixir defimpl FunPark.Monoid, for: FunPark.Monoid.Sum do def empty(_), do: %FunPark.Monoid.Sum{value: 0} def append(%{value: a}, %{value: b}), do: %FunPark.Monoid.Sum{value: a + b} def wrap(_, value), do: %FunPark.Monoid.Sum{value: value} def unwrap(%{value: value}), do: value end ``` ```` Use `Monoid.wrap/2` to lift raw values into the monoid context: ```elixir sum_1 = FunPark.Monoid.wrap(%FunPark.Monoid.Sum{}, 1) ``` ```elixir sum_2 = FunPark.Monoid.wrap(%FunPark.Monoid.Sum{}, 2) ``` Use `append/2` to combine the values within the context of `Monoid.Sum`: ```elixir value = FunPark.Monoid.append(sum_1, sum_2) ``` And unwrap the final value: ```elixir FunPark.Monoid.unwrap(value) ``` ### Appendable ````markdown ```elixir def m_append(monoid, a, b) do monoid |> wrap(a) |> append(wrap(monoid, b)) |> unwrap() end ``` ```` Combine `1` and `2`: ```elixir FunPark.Monoid.Utils.m_append(%FunPark.Monoid.Sum{}, 1, 2) ``` ### Foldable ````markdown ```elixir defprotocol FunPark.Foldable do def foldl(foldable, fun, acc) def foldr(foldable, fun, acc) end ``` ```` ````markdown ```elixir defimpl FunPark.Foldable, for: List do def foldl(list, fun, acc), do: :lists.foldl(fun, acc, list) def foldr(list, fun, acc), do: :lists.foldr(fun, acc, list) end ``` ```` ````markdown ```elixir def m_concat(monoid, list) do empty_value = empty(monoid) FunPark.Foldable.foldl(list, &append/2, empty_value) |> unwrap() end ``` ```` ### Math ````markdown ```elixir def sum(a, b) when is_number(a) and is_number(b) do Utils.m_append(%Monoid.Sum{}, a, b) end def sum(list) when is_list(list) do Utils.m_concat(%Monoid.Sum{}, list) end ``` ```` Now we can add two `Ride` wait time sensors: ```elixir FunPark.Math.sum(1, 2) ``` And we can add a collection of sensors: ```elixir FunPark.Math.sum([1, 2, 3]) ``` Or even a single sensor: ```elixir FunPark.Math.sum([3]) ``` And when there are no sensors at all, the monoid still knows what to do: ```elixir FunPark.Math.sum([]) ``` ## Combine Equality ### Equal All ````markdown ```elixir defmodule FunPark.Monoid.EqAll do defstruct value: true end ``` ```` ````markdown ```elixir defimpl FunPark.Monoid, for: FunPark.Monoid.EqAll do def empty(_), do: %FunPark.Monoid.EqAll{value: true} def append(%{value: a}, %{value: b}), do: %FunPark.Monoid.EqAll{value: a && b} def wrap(_, value), do: %FunPark.Monoid.EqAll{value: value} def unwrap(%{value: value}), do: value end ``` ```` ````markdown ```elixir def append_all(eq_list) do Utils.contramap(fn {a, b} -> Enum.all?(eq_list, fn eq -> eq_map = Utils.to_eq_map(eq) eq_map.eq?.(a, b) end) end) end ``` ```` ````markdown ```elixir def concat_all(eq_list) do append_all(eq_list) end ``` ```` Create two fast passes that share the same ride and time: ```elixir datetime = DateTime.new!(~D[2025-06-01], ~T[13:00:00]) apple = FunPark.Ride.make("Apple Cart") fast_pass_a = FunPark.FastPass.make(apple, datetime) fast_pass_b = FunPark.FastPass.make(apple, datetime) ``` Combine ride and time comparators: ```elixir eq_ride = FunPark.FastPass.eq_ride() eq_time = FunPark.FastPass.eq_time() eq_both = FunPark.Eq.Utils.concat_all([eq_ride, eq_time]) ``` Different IDs make them different: ```elixir FunPark.Eq.Utils.eq?(fast_pass_a, fast_pass_b) ``` ```elixir FunPark.Eq.Utils.eq?(fast_pass_a, fast_pass_b, eq_ride) ``` ```elixir FunPark.Eq.Utils.eq?(fast_pass_a, fast_pass_b, eq_time) ``` ```elixir FunPark.Eq.Utils.eq?(fast_pass_a, fast_pass_b, eq_both) ``` Update the time on `fast_pass_a`: ```elixir datetime_2 = DateTime.new!(~D[2025-06-01], ~T[14:00:00]) fast_pass_a = FunPark.FastPass.change(fast_pass_a, %{time: datetime_2}) ``` Same ride: ```elixir FunPark.Eq.Utils.eq?(fast_pass_a, fast_pass_b, eq_ride) ``` Different time: ```elixir FunPark.Eq.Utils.eq?(fast_pass_a, fast_pass_b, eq_time) ``` Different time and ride: ```elixir FunPark.Eq.Utils.eq?(fast_pass_a, fast_pass_b, eq_both) ``` #### Add to FastPass ````markdown ```elixir def eq_ride_and_time do Eq.Utils.concat_all([eq_ride(), eq_time()]) end ``` ```` ### Equal Any ````markdown ```elixir defmodule FunPark.Monoid.EqAny do defstruct value: false end ``` ```` ````markdown ```elixir defimpl FunPark.Monoid, for: FunPark.Monoid.EqAny do def empty(_), do: %FunPark.Monoid.EqAny{value: false} def append(%{value: a}, %{value: b}), do: %FunPark.Monoid.EqAny{value: a || b} def wrap(_, value), do: %FunPark.Monoid.EqAny{value: value} def unwrap(%{value: value}), do: value end ``` ```` ````markdown ```elixir def duplicate_pass do Eq.Utils.concat_any([ Eq, eq_ride_and_time() ]) end ``` ```` Generate two FastPasses: ```elixir datetime = DateTime.new!(~D[2025-06-01], ~T[13:00:00]) tea_cup = FunPark.Ride.make("Tea Cup") pass_a = FunPark.FastPass.make(tea_cup, datetime) pass_b = FunPark.FastPass.make(tea_cup, datetime) ``` Different IDs: ```elixir FunPark.Eq.Utils.eq?(pass_a, pass_b) ``` Duplicate check considers them the same: ```elixir dup_pass_check = FunPark.FastPass.duplicate_pass() FunPark.Eq.Utils.eq?(pass_a, pass_b, dup_pass_check) ``` Update the first pass to point to a different ride: ```elixir mansion = FunPark.Ride.make("Haunted Mansion") pass_a_changed = FunPark.FastPass.change(pass_a, %{ride: mansion}) ``` Same ID makes them equal: ```elixir FunPark.Eq.Utils.eq?(pass_a, pass_a_changed, dup_pass_check) ``` Different IDs and rides: ```elixir FunPark.Eq.Utils.eq?(pass_b, pass_a_changed, dup_pass_check) ``` ## Combine Order ````markdown ```elixir defmodule FunPark.Monoid.Ord do defstruct lt?: fn _, _ -> false end, le?: fn _, _ -> false end, gt?: fn _, _ -> false end, ge?: fn _, _ -> false end end ``` ```` ````markdown ```elixir def empty(_) do %FunPark.Monoid.Ord{} end def append(ord1, ord2) do %FunPark.Monoid.Ord{ lt?: fn a, b -> cond do ord1.lt?.(a, b) -> true ord1.gt?.(a, b) -> false true -> ord2.lt?.(a, b) end end, le?: fn a, b -> cond do ord1.lt?.(a, b) -> true ord1.gt?.(a, b) -> false true -> ord2.le?.(a, b) end end, gt?: fn a, b -> cond do ord1.gt?.(a, b) -> true ord1.lt?.(a, b) -> false true -> ord2.gt?.(a, b) end end, ge?: fn a, b -> cond do ord1.gt?.(a, b) -> true ord1.lt?.(a, b) -> false true -> ord2.ge?.(a, b) end end } end ``` ```` ````markdown ```elixir def append(ord1, ord2) do Utils.m_append(%Monoid.Ord{}, ord1, ord2) end def concat(ord_list) do Utils.m_concat(%Monoid.Ord{}, ord_list) end ``` ```` Create the patrons Alice, Beth, and Charles: ```elixir alice = FunPark.Patron.make( "Alice", 15, 50, reward_points: 50, ticket_tier: :premium ) beth = FunPark.Patron.make( "Beth", 16, 55, reward_points: 20, ticket_tier: :vip ) charles = FunPark.Patron.make( "Charles", 14, 60, reward_points: 50, ticket_tier: :premium ) ``` Default sort is alphabetical by name: ```elixir FunPark.List.sort([charles, beth, alice]) ``` Priority sort by ticket tier and reward points: ```elixir ord_ticket = FunPark.Patron.ord_by_ticket_tier() ord_reward_points = FunPark.Patron.ord_by_reward_points() ord_priority = FunPark.Ord.Utils.concat([ord_ticket, ord_reward_points]) FunPark.List.sort([charles, beth, alice], ord_priority) ``` Add name as tie-breaker: ```elixir ord_priority = FunPark.Ord.Utils.concat( [ord_ticket, ord_reward_points, FunPark.Ord] ) FunPark.List.sort([charles, beth, alice], ord_priority) ``` ````markdown ```elixir def ord_by_priority do Ord.Utils.concat([ ord_by_ticket_tier(), ord_by_reward_points(), Ord ]) end ``` ```` ## Generalize Maximum ````markdown ```elixir defmodule FunPark.Monoid.Max do defstruct value: nil, ord: FunPark.Ord end ``` ```` ````markdown ```elixir def empty(%{value: identity}), do: %FunPark.Monoid.Max{value: identity} def append(%{value: a, ord: ord}, %{value: b}) do %FunPark.Monoid.Max{value: Ord.Utils.max(a, b, ord), ord: ord} end def wrap(monoid, value), do: %{monoid | value: value} def unwrap(%{value: value}), do: value ``` ```` ````markdown ```elixir def max(a, b) when is_number(a) and is_number(b) do identity = %Monoid.Max{value: Float.min_finite()} Utils.m_append(identity, a, b) end def max(list) when is_list(list) do identity = %Monoid.Max{value: Float.min_finite()} Utils.m_concat(identity, list) end ``` ```` Find the larger of two numbers: ```elixir FunPark.Math.max(1, 2) ``` And `max/1` solves our `Ride` expert's daily report from a ride's wait time log: ```elixir log = [20, 30, 10, 20, 15, 10, 20] FunPark.Math.max(log) ``` Like all Monoids, `Max` also works with a single value: ```elixir FunPark.Math.max([3]) ``` And for an empty list, it returns the identity—in this case, Elixir's smallest possible number: ```elixir FunPark.Math.max([]) ``` ### Prioritize a Patron ````markdown ```elixir def priority_empty do %__MODULE__{ id: nil, name: nil, ticket_tier: nil, reward_points: Float.min_finite(), age: 0, height: 0, fast_passes: [], likes: [], dislikes: [] } end ``` ```` ````markdown ```elixir def max_priority_monoid do %FunPark.Monoid.Max{ value: priority_empty(), ord: ord_by_priority() } end ``` ```` ````markdown ```elixir def highest_priority(patrons) do FunPark.Monoid.Utils.m_concat(max_priority_monoid(), patrons) end ``` ```` Beth has more reward points: ```elixir alice = FunPark.Patron.make("Alice", 15, 150) beth = FunPark.Patron.make("Beth", 15, 150, reward_points: 100) FunPark.Patron.highest_priority([beth, alice]) ``` Alice upgrades to VIP: ```elixir alice = FunPark.Patron.change(alice, %{ticket_tier: :vip}) FunPark.Patron.highest_priority([beth, alice]) ``` The Max Monoid also works when there is only a single patron in the list: ```elixir FunPark.Patron.highest_priority([beth]) ``` If there are no patrons, the sentinel value is returned: ```elixir FunPark.Patron.highest_priority([]) ```
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 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