We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
4. Combine with Monoids
For this chapter we will work with Monoids: identity and combination are going to be the princibles that we work with here.
Identity for an addition/subtraction will be 0 as a + 0 = a
Identity for multiplication/division will be 1 as a * 1 = a
Let’s go over the elements that must be satisfied for this semigroup
A semigroup defines how elements combine and must satisfy two rules:
• Associativity: Grouping doesn’t change the result—a ⊕ (b ⊕ c) = (a ⊕ b) ⊕ c.
• Closure: Combining two elements produces another element of the same kind.
A Monoid adds a third:
• Identity: There’s a neutral element e such that a ⊕ e = a.
This book will adopt a lot of Haskell namespace and naming. But in the end we will define an identity and then a combination.
Define the Protocol
Okay so let’s get started.
#lib/fun_park/monoid.ex
defprotocol FunPark.Monoid do
def empty(monoid_struct)
def append(monoid_struct_a, monoid_struct_b)
def wrap(monoid_struct, value)
def unwrap(monoid_struct)
end
So a _Monoid must define 2 operations:
• empty/1: Returns the identity element. When combined with any value, it
leaves that value unchanged—like adding zero in the context of Sum.
• append/2: Combines two values in an associative way, meaning the grouping
doesn’t affect the result—(a ⊕ b) ⊕ c is the same as a ⊕ (b ⊕ c).
Combine Numbers with Sum
Okay so going from here we need to start to talk about how we would combine a rides wait-time, they might come from different sensors. Let’s start by defining the identity for wait-time.
#lib/fun_park/monoid/sum.ex
defmodule FunPark.Monoid.Sum do
defstruct value: 0
end
New we can implement the Monoid protocol
# same file
defimpl FunPark.Monoid, for: FunPark.Monoid.Sum do
alias FunPark.Monoid.Sum
def empty(_), do: %Sum{}
def append(%Sum{value: a}, %Sum{value: b}) do
%Sum{value: a + b}
end
def wrap(%Sum{}, value) when is_number(value), do: %Sum{value: value}
def unwrap(%Sum{value: value}) when is_number(value), do: value
end
Let’s take some time and go over what we have done:
• empty/1: Returns the identity—an empty Sum struct with a value of 0.
• append/2: Adds the values inside two Sum structs.
• wrap/2: Lifts a number into the monoid struct.
• unwrap/1: Extracts the number from the monoid struct.
Run It
iex -S mix
iex(6)> sum_1 = %FunPark.Monoid.Sum{value: 1}
%FunPark.Monoid.Sum{value: 1}
iex(7)> sum_2 = %FunPark.Monoid.Sum{value: 2}
%FunPark.Monoid.Sum{value: 2}
iex(8)> sum_1 = FunPark.Monoid.wrap(%FunPark.Monoid.Sum{}, 1)
%FunPark.Monoid.Sum{value: 1}
iex(9)> sum_2 = FunPark.Monoid.wrap(%FunPark.Monoid.Sum{}, 2)
%FunPark.Monoid.Sum{value: 2}
iex(10)> value = FunPark.Monoid.append(sum_1, sum_2)
%FunPark.Monoid.Sum{value: 3}
iex(11)> FunPark.Monoid.unwrap(value)
3
Let’s take a min to talk about this. We started by just creating a struct that can work, but this is not always idiomatic so we then used the wrap to created the same sums and then appended them and then unwrapped them to get to the value that we ended up with.
Appendable
Okay so it might make more sense to just use a new function that will append as well as wrap and unwrap the values so that you can just call one function.
#lib/fun_park/monoids/utils.ex
defmodule FunPark.Monoid.Utils do
import FunPark.Monoid, only: [empty: 1, append: 2, wrap: 2, unwrap: 1]
import FunPark.Foldable, only: [fold_l: 3]
def m_append(monoid, a, b) when is_struct(monoid) do
append(wrap(monoid, a), wrap(monoid, b)) |> unwrap()
end
end
Run It
iex(12)> FunPark.Monoid.Utils.m_append(%FunPark.Monoid.Sum{}, 1, 2)
3
Foldable
Now we can add in the fold for a list, it will require 3 elements: the structure, a function to combine, and a base case.
#lib/fun_park/foldable.ex
defprotocol FunPark.Foldable do
def fold_l(structure, transform_fn, base)
def fold_r(structure, transform_fn, base)
end
# lib/fun_park/list.ex
defimpl FunPark.Foldable, for: List do
def fold_l(list, acc, func), do: :lists.foldl(func, acc, list)
def fold_r(list, acc, func), do: :lists.foldr(func, acc, list)
end
Okay so we now have a way to take a list and fold a list from the left or from the right.
Okay so we need to now have a m_concat that will fold and wrap and unwrap for us.
# lib/fun_park/monoids/utils.ex
def m_concat(monoid, values) when is_struct(monoid) and is_list(values) do
fold_l(values, empty(monoid), fn value, acc ->
append(acc, wrap(monoid, value))
end)
|> unwrap()
end
Math
So we have ways of dealing with append which is a sum function but it might be better to have an outer layer that will allow a user to use the function while not exposing the inners. We can create a Math module for this purpose.
# lib/fun_park/math.ex
defmodule FunPark.Math do
import FunPark.Monoid.Utils, only: [m_append: 3, m_concat: 2]
alias FunPark.Monoid
def sum(a, b) do
m_append(%Monoid.Sum{}, a, b)
end
def sum(list) when is_list(list) do
m_concat(%Monoid.Sum{}, list)
end
end
Run It
iex(13)> FunPark.Math.sum(1, 2)
3
iex(14)> FunPark.Math.sum([1, 2, 3])
6
iex(15)> FunPark.Math.sum([3])
3
iex(16)> FunPark.Math.sum([])
0
Lot’s to go over here with such little tests but we can see that we get to see that if we pass a list we get the sum of all the list, or a set of 2 values we get the sum of those values. This is done by either calling the m_append/3 or the m_concat/3 for values or a list. Now remember that this will not just be set to combining concrete values we can set ways to “add or subtract” based off of anything that we define.
Combine Equality
You can combine equality in two ways:
• All: Every equality check must return true.
• Any: At least one equality check must return true.
Equal All
So there might be a time that we create a duplicate FastPass so we need a way to determining if there is a duplicate. A way to do this might be just to use the eq?/2 and run through each pass against the other but that might not be easily repeatable.
Construct the Monoid
First the struct.
# lib/fun_park/monoid/eq_all.ex
defmodule FunPark.Monoid.Eq.All do
defstruct eq?: &FunPark.Monoid.Eq.All.default_eq?/2,
not_eq?: &FunPark.Monoid.Eq.All.default_not_eq?/2
def default_eq?(_, _), do: true
def default_not_eq?(_, _), do: false
end
Now the monoid:
# Same file
defimpl FunPark.Monoid, for: FunPark.Monoid.Eq.All do
alias FunPark.Eq.Utils
alias FunPark.Monoid.Eq.All
def empty(_), do: %All{}
def append(%All{} = eq1, %All{} = eq2) do
%All{
eq?: fn a, b -> eq1.eq?.(a, b) && eq2.eq?.(a, b) end,
not_eq?: fn a, b -> eq1.not_eq?.(a, b) || eq2.not_eq?.(a, b) end
}
end
def wrap(%All{}, eq) do
eq = Utils.to_eq_map(eq)
%All{
eq?: eq.eq?,
not_eq?: eq.not_eq?
}
end
def unwrap(%All{eq?: eq?, not_eq?: not_eq?}) do
%{eq?: eq?, not_eq?: not_eq?}
end
end
Okay so we have the check with && for the eq? so that all the checks need to be true. And then || for not_eq/2 so that it fails if one isn’t true.
Now we need to add in a utils that can work for the append and concat.
# lib/fun_park/eq/utils.ex
def append_all(a, b) do
m_append(%Monoid.Eq.All{}, a, b)
end
def concat_all(eq_list) when is_list(eq_list) do
m_concat(%Monoid.Eq.All{}, eq_list)
end
Run It
iex(17)> datetime = DateTime.new!(~D[2025-06-01], ~T[13:00:00])
~U[2025-06-01 13:00:00Z]
iex(18)> apple = FunPark.Ride.make("Apple Cart")
%FunPark.Ride{
id: 131,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
iex(20)> fast_pass_a = FunPark.FastPass.make(apple, datetime)
%FunPark.FastPass{
id: 259,
ride: %FunPark.Ride{
id: 131,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
},
time: ~U[2025-06-01 13:00:00Z]
}
iex(21)> fast_pass_b = FunPark.FastPass.make(apple, datetime)
%FunPark.FastPass{
id: 323,
ride: %FunPark.Ride{
id: 131,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
},
time: ~U[2025-06-01 13:00:00Z]
}
iex(22)> eq_ride = FunPark.FastPass.eq_ride()
%{
eq?: #Function<0.114186896/2 in FunPark.Eq.Utils.contramap/2>,
not_eq?: #Function<1.114186896/2 in FunPark.Eq.Utils.contramap/2>
}
iex(23)> eq_time = FunPark.FastPass.eq_time()
%{
eq?: #Function<0.114186896/2 in FunPark.Eq.Utils.contramap/2>,
not_eq?: #Function<1.114186896/2 in FunPark.Eq.Utils.contramap/2>
}
iex(24)> eq_both = FunPark.Eq.Utils.concat_all([eq_ride, eq_time])
%{
eq?: #Function<0.92338739/2 in FunPark.Monoid.FunPark.Monoid.Eq.All.append/2>,
not_eq?: #Function<1.92338739/2 in FunPark.Monoid.FunPark.Monoid.Eq.All.append/2>
}
iex(25)> FunPark.Eq.Utils.eq?(fast_pass_a, fast_pass_b)
false
iex(26)> FunPark.Eq.Utils.eq?(fast_pass_a, fast_pass_b, eq_ride)
true
iex(27)> FunPark.Eq.Utils.eq?(fast_pass_a, fast_pass_b, eq_time)
true
iex(28)> FunPark.Eq.Utils.eq?(fast_pass_a, fast_pass_b, eq_both)
true
iex(29)> datetime_2 = DateTime.new!(~D[2025-06-01], ~T[14:00:00])
~U[2025-06-01 14:00:00Z]
iex(30)> fast_pass_a = FunPark.FastPass.change(fast_pass_a, %{time: datetime_2})
%FunPark.FastPass{
id: 259,
ride: %FunPark.Ride{
id: 131,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
},
time: ~U[2025-06-01 14:00:00Z]
}
iex(31)> FunPark.Eq.Utils.eq?(fast_pass_a, fast_pass_b, eq_ride)
true
iex(32)> FunPark.Eq.Utils.eq?(fast_pass_a, fast_pass_b, eq_time)
false
iex(33)> FunPark.Eq.Utils.eq?(fast_pass_a, fast_pass_b, eq_both)
false
Okay so we created a ride and two FastPass for the ride. Then we used the different comparators to test the different passes against the values. This is all things that we have done before.
Add to FastPass
# lib/fun_park/fast_pass.ex
def eq_ride_and_time do
Eq.Utils.concat_all([eq_ride(), eq_time()])
end
Equal Any
This might work for just the values of ride_time or ride_id but we also want to test for FastPass id as well.
Construct the Monoid
# lib/fun_park/monoid/eq_any.ex
defmodule FunPark.Monoid.Eq.Any do
defstruct eq?: &FunPark.Monoid.Eq.Any.default_eq?/2,
not_eq?: &FunPark.Monoid.Eq.Any.default_not_eq?/2
def default_eq?(_, _), do: false
def default_not_eq?(_, _), do: true
end
# Now the monoid same file
defimpl FunPark.Monoid, for: FunPark.Monoid.Eq.Any do
alias FunPark.Eq.Utils
alias FunPark.Monoid.Eq.Any
def empty(_), do: %Any{}
def append(%Any{} = eq1, %Any{} = eq2) do
%Any{
eq?: fn a, b -> eq1.eq?.(a, b) || eq2.eq?.(a, b) end,
not_eq?: fn a, b -> eq1.not_eq?.(a, b) && eq2.not_eq?.(a, b) end
}
end
def wrap(%Any{}, eq) do
eq = Utils.to_eq_map(eq)
%Any{
eq?: eq.eq?,
not_eq?: eq.not_eq?
}
end
def unwrap(%Any{eq?: eq?, not_eq?: not_eq?}) do
%{eq?: eq?, not_eq?: not_eq?}
end
end
Same logic for the append so that any of the values are true it will return true. Then the not_eq/2 will return if and only if all values will not return true. Now let’s move onto the utils.ex so we can extract some of the lower level functionality.
def append_any(a, b) do
m_append(%Monoid.Eq.Any{}, a, b)
end
def concat_any(eq_list) when is_list(eq_list) do
m_concat(%Monoid.Eq.Any{}, eq_list)
end
Now we can try and track any values that we want to compare. Let’s take it for a test spin
Run It
iex(34)> datetime = DateTime.new!(~D[2025-06-01], ~T[13:00:00])
~U[2025-06-01 13:00:00Z]
iex(35)> tea_cup = FunPark.Ride.make("Tea Cup")
%FunPark.Ride{
id: 643,
name: "Tea Cup",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
iex(36)> pass_a = FunPark.FastPass.make(tea_cup, datetime
...(36)> )
%FunPark.FastPass{
id: 707,
ride: %FunPark.Ride{
id: 643,
name: "Tea Cup",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
},
time: ~U[2025-06-01 13:00:00Z]
}
iex(37)> pass_b = FunPark.FastPass.make(tea_cup, datetime)
%FunPark.FastPass{
id: 771,
ride: %FunPark.Ride{
id: 643,
name: "Tea Cup",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
},
time: ~U[2025-06-01 13:00:00Z]
}
iex(38)> FunPark.Eq.Utils.eq?(pass_a, pass_b)
false
iex(39)> dup_pass_check = FunPark.FastPass.duplicate_pass()
%{
eq?: #Function<0.131157824/2 in FunPark.Monoid.FunPark.Monoid.Eq.Any.append/2>,
not_eq?: #Function<1.131157824/2 in FunPark.Monoid.FunPark.Monoid.Eq.Any.append/2>
}
iex(40)> FunPark.Eq.Utils.eq?(pass_a, pass_b, dup_pass_check)
true
iex(41)> mansion = FunPark.Ride.make("Haunted Mansion")
%FunPark.Ride{
id: 835,
name: "Haunted Mansion",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
iex(42)> pass_a_changed = FunPark.FastPass.change(pass_a, %{ride: mansion})
%FunPark.FastPass{
id: 707,
ride: %FunPark.Ride{
id: 835,
name: "Haunted Mansion",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
},
time: ~U[2025-06-01 13:00:00Z]
}
iex(43)> FunPark.Eq.Utils.eq?(pass_a, pass_a_changed, dup_pass_check)
true
iex(44)> FunPark.Eq.Utils.eq?(pass_b, pass_a_changed, dup_pass_check)
false
We added in the new duplicate_pass/0 that allows us to check if both ride and time are the same. Then we created a new ride and passes for it. They are not the same for eq? as they don’t have the same id but for the duplicate_pass they are the same as they share the same ride and time. Then we wanted to check after the ride is different. Again the eq? is fine as they are the same id but the duplicate_pass says they are not the same as the rides are now different.
Combine Order
Okay so now we want to have an ability to define the priority of a patron first by ticket then by points, so we need a way to combine.
Construct the Monoid
As always first the struct then the monoid.
# lib/fun_park/monoid/ord.ex
defmodule FunPark.Monoid.Ord do
defstruct lt?: &FunPark.Monoid.Ord.default?/2,
le?: &FunPark.Monoid.Ord.default?/2,
gt?: &FunPark.Monoid.Ord.default?/2,
ge?: &FunPark.Monoid.Ord.default?/2
def default?(_, _), do: false
end
#monoid
defimpl FunPark.Monoid, for: FunPark.Monoid.Ord do
alias FunPark.Monoid.Ord
alias FunPark.Ord.Utils
def empty(_) do
%Ord{}
end
def append(%Ord{} = ord1, %Ord{} = ord2) do
%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
def wrap(%Ord{}, ord) do
ord = Utils.to_ord_map(ord)
%Ord{
lt?: ord.lt?,
le?: ord.le?,
gt?: ord.gt?,
ge?: ord.ge?
}
end
def unwrap(%Ord{lt?: lt?, le?: le?, gt?: gt?, ge?: ge?}) do
%{
lt?: lt?,
le?: le?,
gt?: gt?,
ge?: ge?
}
end
end
Breaking It All Down
Okay so struct defines the default, the monoid will define the empty append wrap and unwrap
We can now take a look at the lt?/2. We have some cases if it is lt? then true. if gt? then false, if is not either then we can check against ord2 which will start the process again from the other prospective assuming that the others in the list were equal.
Abstract the Monoid
# lib/fun_park/ord/utils.ex
def append(a, b) do
m_append(%FunPark.Monoid.Ord{}, a, b)
end
def concat(ord_list) when is_list(ord_list) do
m_concat(%FunPark.Monoid.Ord{}, ord_list)
end
Run It
iex(45)> alice = FunPark.Patron.make(
...(45)> "Alice", 15, 50, reward_points: 50, ticket_tier: :premium
...(45)> )
%FunPark.Patron{
id: 899,
name: "Alice",
age: 15,
height: 50,
ticket_tier: :premium,
fast_passes: [],
reward_points: 50,
likes: [],
dislikes: []
}
iex(46)> beth = FunPark.Patron.make(
...(46)> "Beth", 16, 55, reward_points: 20, ticket_tier: :vip
...(46)> )
%FunPark.Patron{
id: 963,
name: "Beth",
age: 16,
height: 55,
ticket_tier: :vip,
fast_passes: [],
reward_points: 20,
likes: [],
dislikes: []
}
iex(47)> charles = FunPark.Patron.make(
...(47)> "Charles", 14, 60, reward_points: 50, ticket_tier: :premium
...(47)> )
%FunPark.Patron{
id: 1027,
name: "Charles",
age: 14,
height: 60,
ticket_tier: :premium,
fast_passes: [],
reward_points: 50,
likes: [],
dislikes: []
}
iex(48)> FunPark.List.sort([charles, beth, alice])
[
%FunPark.Patron{
id: 899,
name: "Alice",
age: 15,
height: 50,
ticket_tier: :premium,
fast_passes: [],
reward_points: 50,
likes: [],
dislikes: []
},
%FunPark.Patron{
id: 963,
name: "Beth",
age: 16,
height: 55,
ticket_tier: :vip,
fast_passes: [],
reward_points: 20,
likes: [],
dislikes: []
},
%FunPark.Patron{
id: 1027,
name: "Charles",
age: 14,
height: 60,
ticket_tier: :premium,
fast_passes: [],
reward_points: 50,
likes: [],
dislikes: []
}
]
iex(49)> ord_ticket = FunPark.Patron.ord_by_ticket_tier()
%{
lt?: #Function<1.94864313/2 in FunPark.Ord.Utils.contramap/2>,
gt?: #Function<3.94864313/2 in FunPark.Ord.Utils.contramap/2>,
ge?: #Function<4.94864313/2 in FunPark.Ord.Utils.contramap/2>,
le?: #Function<2.94864313/2 in FunPark.Ord.Utils.contramap/2>
}
iex(50)> ord_reward_points = FunPark.Patron.ord_by_reward_points()
%{
lt?: #Function<1.94864313/2 in FunPark.Ord.Utils.contramap/2>,
gt?: #Function<3.94864313/2 in FunPark.Ord.Utils.contramap/2>,
ge?: #Function<4.94864313/2 in FunPark.Ord.Utils.contramap/2>,
le?: #Function<2.94864313/2 in FunPark.Ord.Utils.contramap/2>
}
iex(51)> ord_priority = FunPark.Ord.Utils.concat([ord_ticket, ord_reward_points])
%{
lt?: #Function<0.54941642/2 in FunPark.Monoid.FunPark.Monoid.Ord.append/2>,
gt?: #Function<2.54941642/2 in FunPark.Monoid.FunPark.Monoid.Ord.append/2>,
ge?: #Function<3.54941642/2 in FunPark.Monoid.FunPark.Monoid.Ord.append/2>,
le?: #Function<1.54941642/2 in FunPark.Monoid.FunPark.Monoid.Ord.append/2>
}
iex(52)> FunPark.List.sort([charles, beth, alice], ord_priority)
[
%FunPark.Patron{
id: 1027,
name: "Charles",
age: 14,
height: 60,
ticket_tier: :premium,
fast_passes: [],
reward_points: 50,
likes: [],
dislikes: []
},
%FunPark.Patron{
id: 899,
name: "Alice",
age: 15,
height: 50,
ticket_tier: :premium,
fast_passes: [],
reward_points: 50,
likes: [],
dislikes: []
},
%FunPark.Patron{
id: 963,
name: "Beth",
age: 16,
height: 55,
ticket_tier: :vip,
fast_passes: [],
reward_points: 20,
likes: [],
dislikes: []
}
]
iex(53)> ord_priority = FunPark.Ord.Utils.concat(
...(53)> [ord_ticket, ord_reward_points, FunPark.Ord]
...(53)> )
%{
lt?: #Function<0.54941642/2 in FunPark.Monoid.FunPark.Monoid.Ord.append/2>,
gt?: #Function<2.54941642/2 in FunPark.Monoid.FunPark.Monoid.Ord.append/2>,
ge?: #Function<3.54941642/2 in FunPark.Monoid.FunPark.Monoid.Ord.append/2>,
le?: #Function<1.54941642/2 in FunPark.Monoid.FunPark.Monoid.Ord.append/2>
}
iex(54)> FunPark.List.sort([charles, beth, alice], ord_priority)
[
%FunPark.Patron{
id: 899,
name: "Alice",
age: 15,
height: 50,
ticket_tier: :premium,
fast_passes: [],
reward_points: 50,
likes: [],
dislikes: []
},
%FunPark.Patron{
id: 1027,
name: "Charles",
age: 14,
height: 60,
ticket_tier: :premium,
fast_passes: [],
reward_points: 50,
likes: [],
dislikes: []
},
%FunPark.Patron{
id: 963,
name: "Beth",
age: 16,
height: 55,
ticket_tier: :vip,
fast_passes: [],
reward_points: 20,
likes: [],
dislikes: []
}
]
Okay so we have the default sort based off of names of the Patrons then we added in some sort priorities that worked almost perfect. However the priority doesn’t fall back on the name after so we added that into the list of priorities.
Let’s create that all into one function so that we can just pull that anytime we want.
# lib/fun_park/patron.ex
def ord_by_priority do
Ord.Utils.concat([
ord_by_ticket_tier(),
ord_by_reward_points(),
Ord
])
end
Generalize Maximum
Okay so now we want to use the sorts that we have from before and have the ablity to find the maximum.
Construct the Monoid
# lib/fun_park/monoid/max.ex
defmodule FunPark.Monoid.Max do
defstruct value: nil, ord: FunPark.Ord
end
defimpl FunPark.Monoid, for: FunPark.Monoid.Max do
alias FunPark.Monoid.Max
alias FunPark.Ord.Utils
def empty(%Max{value: min_value, ord: ord}) do
%Max{value: min_value, ord: ord}
end
def append(%Max{value: a, ord: ord}, %Max{value: b}) do
%Max{value: Utils.max(a, b, ord), ord: ord}
end
def wrap(%Max{ord: ord}, value) do
%Max{value: value, ord: Utils.to_ord_map(ord)}
end
def unwrap(%Max{value: value}), do: value
end
For the max that we made in the ord/utils we are using that here.
Abstract the Monoid
# lib/fun_park/math.ex
def max(a, b) do
m_append(%Monoid.Max{value: Float.min_finite()}, a, b)
end
def max(list) when is_list(list) do
m_concat(%Monoid.Max{value: Float.min_finite()}, list)
end
Run It
iex(55)> FunPark.Math.max(1, 2)
2
iex(56)> log = [20, 30, 10, 20, 15, 10, 20]
[20, 30, 10, 20, 15, 10, 20]
iex(57)> FunPark.Math.max(log)
30
iex(58)> FunPark.Math.max([3])
3
iex(59)> FunPark.Math.max([])
-1.7976931348623157e308
Okay we are still leveraging the append and concat from before and then we are even able to send in an empty list and it will return elixirs smallest number.
What about Elixir’s Built-In Max?
There is not custom type for this and then there is not identity for what happens when you pass in an empty list.
Prioritize a Patron Okay so now we need to implement a way to have a sorted queue so that we can pull the highest and then so on. We will follow the same logic but keeping in mind that we have some functions already defined.
# lib/fun_park/patron.ex
def priority_empty do
%__MODULE__{reward_points: Float.min_finite(), ticket_tier: nil}
end
defp max_priority_monoid do
%Monoid.Max{
value: priority_empty(),
ord: ord_by_priority()
}
end
def highest_priority(patrons) when is_list(patrons) do
m_concat(max_priority_monoid(), patrons)
end
We have the identity with the priority_empty/0 we have the monoid with max_priority_monoid/0 then we have a way to abstract the Monoid with highest_priority/1.
The highest_priority/1 function folds a list of patrons, returning the one with the
highest priority.
• Ord defines how to compare patrons—first by ticket tier, then by reward
points.
• Max uses that comparison to keep the greater of two patrons.
• The Monoid protocol provides a consistent interface for combining values.
• m_concat/2 performs the reduction.
• highest_priority/1 abstracts all these internal details from the caller.
Run It
iex(60)> alice = FunPark.Patron.make("Alice", 15, 150)
%FunPark.Patron{
id: 1091,
name: "Alice",
age: 15,
height: 150,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(61)> beth = FunPark.Patron.make("Beth", 15, 150, reward_points: 100)
%FunPark.Patron{
id: 1155,
name: "Beth",
age: 15,
height: 150,
ticket_tier: :basic,
fast_passes: [],
reward_points: 100,
likes: [],
dislikes: []
}
iex(62)> FunPark.Patron.highest_priority([beth, alice])
%FunPark.Patron{
id: 1155,
name: "Beth",
age: 15,
height: 150,
ticket_tier: :basic,
fast_passes: [],
reward_points: 100,
likes: [],
dislikes: []
}
iex(63)> alice = FunPark.Patron.change(alice, %{ticket_tier: :vip})
%FunPark.Patron{
id: 1091,
name: "Alice",
age: 15,
height: 150,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(64)> FunPark.Patron.highest_priority([beth, alice])
%FunPark.Patron{
id: 1091,
name: "Alice",
age: 15,
height: 150,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(65)> FunPark.Patron.highest_priority([beth])
%FunPark.Patron{
id: 1155,
name: "Beth",
age: 15,
height: 150,
ticket_tier: :basic,
fast_passes: [],
reward_points: 100,
likes: [],
dislikes: []
}
iex(66)> FunPark.Patron.highest_priority([])
%FunPark.Patron{
id: nil,
name: nil,
age: 0,
height: 0,
ticket_tier: nil,
fast_passes: [],
reward_points: -1.7976931348623157e308,
likes: [],
dislikes: []
}
Okay so we have new patrons that were created and then we found the max, changed the ticket_tier of one and check again. We tested against a single patron then no patron.
Manage Complexity
Let’s take a min to focus on cyclomatic complexity which will measure the number of independent paths through a function.
• A score of 1–4 indicates simple logic that is easy to understand and
maintain.
• A score of 5–10 suggests moderate complexity, where changes require
careful consideration.
• A score above 10 signals high complexity, making the function difficult
to extend and maintain.
The Hidden cost of Imperative Code
Here is an imperative approach to finding the order of a set of Patrons
def ord_by_priority(patrons) do
Enum.sort(patrons, fn a, b ->
cond do
a.ticket_tier == :vip and b.ticket_tier != :vip ->
true
b.ticket_tier == :vip and a.ticket_tier != :vip ->
false
a.ticket_tier == :premium and b.ticket_tier not in [:vip, :premium] ->
true
b.ticket_tier == :premium and a.ticket_tier not in [:vip, :premium] ->
false
a.reward_points < b.reward_points ->
true
a.reward_points > b.reward_points ->
false
a.name > b.name ->
true
a.name < b.name ->
false
true ->
false
end
end)
end
This will get a score of 9 as there are 9 paths through the function. There are so many ways to mess up once you add in an other path through the function.
More What You’d Call “Guidelines” Than Actual Rules
This is all about how much work will be involved fixing or changing code when new tasks are needed.
Reduce Complexity with Declarative Logic
def ord_by_priority do
ord_by_ticket_tier()
|> append(ord_by_reward_points())
|> append(Ord)
end
Here is an example of a declarative logic approach. It is more idiomatic as an elixir developer and it adds more ways to deal with anything you would do in code.
What You’ve Learned
We have a way to define Eq and Ord but with monoids we are able to take that and then define ways of combining and returning an identity. These aren’t just abstractions they are mental models with code behind them.