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)

# Implement Domain-Specific Equality with Protocols ```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 2**<br/>[Advanced Functional Programming with Elixir](https://pragprog.com/titles/jkelixir/advanced-functional-programming-with-elixir). | ## Polymorphic Equality ````markdown ```elixir defprotocol FunPark.Eq do def eq?(a, b) def not_eq?(a, b) end ``` ```` ````markdown ```elixir defimpl FunPark.Eq, for: Any do def eq?(a, b), do: a == b def not_eq?(a, b), do: a != b end ``` ```` ```elixir 1 == 1 ``` ```elixir 1 == 2 ``` ```elixir FunPark.Eq.eq?(1, 1) ``` ```elixir FunPark.Eq.eq?(1, 2) ``` ## Implement Equality for FunPark Contexts ````markdown ```elixir def change(%__MODULE__{} = patron, attrs) when is_map(attrs) do attrs = Map.delete(attrs, :id) struct(patron, attrs) end ``` ```` Create a patron named Alice: ```elixir alice_a = FunPark.Patron.make("Alice", 15, 150) ``` Alice decides to update her ticket to `:premium`: ```elixir alice_b = FunPark.Patron.change(alice_a, %{ticket_tier: :premium}) ``` From Elixir's perspective, `alice_a` and `alice_b` are not equal: ```elixir alice_a == alice_b ``` However, within the `Patron` context, this is wrong. Upgrading Alice's ticket doesn't make her a different patron; it just reflects a change in her attributes. ### Implement the Eq Protocol ````markdown ```elixir defimpl FunPark.Eq, for: FunPark.Patron do alias FunPark.Eq alias FunPark.Patron def eq?(%Patron{id: v1}, %Patron{id: v2}), do: Eq.eq?(v1, v2) def not_eq?(%Patron{id: v1}, %Patron{id: v2}), do: Eq.not_eq?(v1, v2) end ``` ```` First, let's regenerate our `Patron` structs: ```elixir alice_a = FunPark.Patron.make("Alice", 15, 150) alice_b = FunPark.Patron.change(alice_a, %{ticket_tier: :premium}) ``` And FunPark knows that although these are different records, they refer to the same `Patron`: ```elixir FunPark.Eq.eq?(alice_a, alice_b) ``` ### Ride Eq ````markdown ```elixir def change(%__MODULE__{} = ride, attrs) when is_map(attrs) do attrs = Map.delete(attrs, :id) struct(ride, attrs) end ``` ```` ````markdown ```elixir defimpl FunPark.Eq, for: FunPark.Ride do alias FunPark.Eq alias FunPark.Ride def eq?(%Ride{id: v1}, %Ride{id: v2}), do: Eq.eq?(v1, v2) def not_eq?(%Ride{id: v1}, %Ride{id: v2}), do: Eq.not_eq?(v1, v2) end ``` ```` Let's generate a ride: ```elixir ride_a = FunPark.Ride.make("Dark Mansion", min_age: 14, tags: [:dark]) ``` And update the wait time to 20 minutes: ```elixir ride_b = FunPark.Ride.change(ride_a, %{wait_time: 20}) ``` Again, from Elixir's perspective, these are not equal: ```elixir ride_a == ride_b ``` But from the Ride context, updating the wait time does not make them different rides: ```elixir FunPark.Eq.eq?(ride_a, ride_b) ``` ### FastPass Eq ````markdown ```elixir def change(%__MODULE__{} = fast_pass, attrs) when is_map(attrs) do attrs = Map.delete(attrs, :id) struct(fast_pass, attrs) end ``` ```` ````markdown ```elixir defimpl FunPark.Eq, for: FunPark.FastPass do alias FunPark.Eq alias FunPark.FastPass def eq?(%FastPass{id: v1}, %FastPass{id: v2}), do: Eq.eq?(v1, v2) def not_eq?(%FastPass{id: v1}, %FastPass{id: v2}), do: Eq.not_eq?(v1, v2) end ``` ```` Generate a fast pass: ```elixir tea_cup = FunPark.Ride.make("Tea Cup") datetime = DateTime.new!(~D[2025-06-01], ~T[13:00:00]) pass_a = FunPark.FastPass.make(tea_cup, datetime) ``` Now update the ride on that pass: ```elixir haunted_mansion = FunPark.Ride.make("Haunted Mansion") pass_b = FunPark.FastPass.change(pass_a, %{ride: haunted_mansion}) ``` Elixir sees the difference: ```elixir pass_a == pass_b ``` But our `Eq` implementation focuses only on ID and treats them as equal: ```elixir FunPark.Eq.eq?(pass_a, pass_b) ``` ## Transform Inputs Before Matching After implementing the logic for fast passes, our `Patron` expert points out a problem—a patron can't be in two places at once. From their perspective, two passes scheduled for the same time are effectively duplicates. ````markdown ```elixir defmodule FunPark.Eq.Utils do alias FunPark.Eq def contramap(f) do %{ eq?: fn a, b -> Eq.eq?(f.(a), f.(b)) end, not_eq?: fn a, b -> Eq.not_eq?(f.(a), f.(b)) end } end end ``` ```` ````markdown ```elixir def get_time(%__MODULE__{time: time}), do: time def eq_time do Eq.Utils.contramap(&get_time/1) end ``` ```` Create the rides: ```elixir mansion = FunPark.Ride.make("Dark Mansion", min_age: 14, tags: [:dark]) tea_cup = FunPark.Ride.make("Tea Cup") ``` Generate a fast pass for the Dark Mansion: ```elixir datetime = DateTime.new!(~D[2025-06-01], ~T[13:00:00]) fast_pass_a = FunPark.FastPass.make(mansion, datetime) ``` Generate another Fast Pass for the Tea Cup using the same time: ```elixir fast_pass_b = FunPark.FastPass.make(tea_cup, datetime) ``` Our default equality check knows these are different (they have different ids): ```elixir FunPark.Eq.eq?(fast_pass_a, fast_pass_b) ``` But our new custom equality knows they have the same time: ```elixir FunPark.FastPass.eq_time().eq?.(fast_pass_a, fast_pass_b) ``` ### Simplify Equality Checks ````markdown ```elixir def eq?(a, b, eq \\ Eq) do eq = to_eq_map(eq) eq.eq?.(a, b) end ``` ```` Let's regenerate our passes: ```elixir mansion = FunPark.Ride.make("Dark Mansion", min_age: 14, tags: [:dark]) tea_cup = FunPark.Ride.make("Tea Cup") datetime = DateTime.new!(~D[2025-06-01], ~T[13:00:00]) fast_pass_a = FunPark.FastPass.make(mansion, datetime) fast_pass_b = FunPark.FastPass.make(tea_cup, datetime) ``` From the context of `FastPass`, the passes have different ID's, so they are different: ```elixir FunPark.Eq.Utils.eq?(fast_pass_a, fast_pass_b) ``` But we can inject our `eq_time/0` logic to determine they are scheduled for the same time: ```elixir has_eq_time = FunPark.FastPass.eq_time() FunPark.Eq.Utils.eq?(fast_pass_a, fast_pass_b, has_eq_time) ``` ## Harness Equality for Collections ### Unique FunPark tracks the number of patrons entering per day, but some patrons leave and re-enter, so we need a unique count. ````markdown ```elixir def uniq(list, eq \\ Eq) do eq = Utils.to_eq_map(eq) Enum.reduce(list, [], fn item, acc -> if Enum.any?(acc, &eq.eq?.(item, &1)) do acc else acc ++ [item] end end) end ``` ```` First, let's make two copies of Alice: ```elixir alice_a = FunPark.Patron.make("Alice", 15, 50) alice_b = FunPark.Patron.change(alice_a, %{ticket_tier: :premium}) FunPark.List.uniq([alice_a, alice_b]) ``` Even though `alice_a` and `alice_b` have different ticket tiers, they share the same ID. Since `uniq/1` is context-aware, it used the `Eq` implementation for `Patron` and removed the second entry. ### Union FunPark tracks ride downtime from multiple sources, including scheduled maintenance and unexpected breakdowns. At the end of the day, we need to combine these logs to generate a unique list of rides that were offline. ````markdown ```elixir def union(list_a, list_b, eq \\ Eq) do uniq(list_a ++ list_b, eq) end ``` ```` ```elixir tea_cup = FunPark.Ride.make("Tea Cup") haunted_mansion = FunPark.Ride.make("Haunted Mansion") apple_cart = FunPark.Ride.make("Apple Cart") maintenance_log = [haunted_mansion, apple_cart] breakdown_log = [tea_cup, haunted_mansion] FunPark.List.union(maintenance_log, breakdown_log) ``` This combines both lists into one, removing duplicates based on the `Ride`'s `Eq` implementation. ### Intersection FunPark adjusts incentives to balance crowd flow, steering guests away from overbooked rides. By intersecting rides with the longest wait times and rides with the most Fast Pass bookings, we identify high-demand attractions. ````markdown ```elixir def intersection(list_a, list_b, eq \\ Eq) do eq = Utils.to_eq_map(eq) Enum.filter(list_a, fn item_a -> Enum.any?(list_b, &eq.eq?.(item_a, &1)) end) |> uniq(eq) end ``` ```` The Haunted Mansion is in high demand: ```elixir long_wait = [haunted_mansion, apple_cart] most_fast_pass = [tea_cup, haunted_mansion] FunPark.List.intersection(long_wait, most_fast_pass) ``` ### Difference Not all rides in FunPark are accessible to every patron. We can use difference to subtract restricted rides from the full list, identifying attractions available to all guests. ````markdown ```elixir def difference(list_a, list_b, eq \\ Eq) do eq = Utils.to_eq_map(eq) Enum.filter(list_a, fn item_a -> not Enum.any?(list_b, &eq.eq?.(item_a, &1)) end) end ``` ```` Apple Cart is available to everyone: ```elixir all_rides = [haunted_mansion, apple_cart] restricted_rides = [haunted_mansion] FunPark.List.difference(all_rides, restricted_rides) ``` ### Subset FunPark needs to track if guests made full use of their Fast Passes. We can check if every eligible ride was used by verifying that the patron's Fast Pass list is a subset of the rides they rode. ````markdown ```elixir def subset?(list_a, list_b, eq \\ Eq) do eq = Utils.to_eq_map(eq) Enum.all?(list_a, fn item_a -> Enum.any?(list_b, &eq.eq?.(item_a, &1)) end) end ``` ```` We find that our patron has indeed completed all of their Fast Pass rides: ```elixir fast_pass_rides = [tea_cup] rides_completed = [haunted_mansion, tea_cup] FunPark.List.subset?(fast_pass_rides, rides_completed) ```
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