We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
3. Create Flexible Ordering with Protocols
Now we want to talk about ordering for our Contexts. Let’s start out with the rules.
• Reflexivity: a ≤ a.
• Antisymmetry: If a ≤ b and b ≤ a, then a = b.
• Transitivity: If a ≤ b and b ≤ c, then a ≤ c.
Define Order with a Protocol
Let’s start by adding in an other Protocol for the order.
defprotocol FunPark.Ord do
@fallback_to_any true
def lt?(a, b)
def le?(a, b)
def gt?(a, b)
def ge?(a, b)
end
# Now the implementation for those
defimpl FunPark.Ord, for: Any do
def lt?(a, b), do: a < b
def le?(a, b), do: a <= b
def gt?(a, b), do: a > b
def ge?(a, b), do: a >= b
end
Okay so for this we have defined: lt? (less than), le? (less than or equal), gt? (greater than), ge? (greater than or equal).
Run It
iex(1)> 1 < 2
true
iex(2)> 1 > 2
false
iex(3)> FunPark.Ord.lt?(1, 2)
true
iex(4)> FunPark.Ord.gt?(1, 2)
false
Runs as expected for the inputs.
Implement Order for FunPark Contexts
Okay so let’s do the same for all the Context.
Ride Context
defimpl FunPark.Ord, for: FunPark.Ride do
alias FunPark.Ord
alias FunPark.Ride
def lt?(%Ride{name: v1}, %Ride{name: v2}), do: Ord.lt?(v1, v2)
def le?(%Ride{name: v1}, %Ride{name: v2}), do: Ord.le?(v1, v2)
def gt?(%Ride{name: v1}, %Ride{name: v2}), do: Ord.gt?(v1, v2)
def ge?(%Ride{name: v1}, %Ride{name: v2}), do: Ord.ge?(v1, v2)
end
We should be getting the syntax by now but let’s run it.
Run It
iex(5)> banana_slip = FunPark.Ride.make("Banana Slip")
%FunPark.Ride{
id: 3138,
name: "Banana Slip",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
iex(6)> apple_cart = FunPark.Ride.make("Apple Cart")
%FunPark.Ride{
id: 3202,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
iex(7)> apple_cart < banana_slip
false
iex(8)> FunPark.Ord.lt?(apple_cart, banana_slip)
true
FastPass Context
defimpl FunPark.Ord, for: FunPark.FastPass do
alias FunPark.Ord
alias FunPark.FastPass
def lt?(%FastPass{time: v1}, %FastPass{time: v2}), do: Ord.lt?(v1, v2)
def le?(%FastPass{time: v1}, %FastPass{time: v2}), do: Ord.le?(v1, v2)
def gt?(%FastPass{time: v1}, %FastPass{time: v2}), do: Ord.gt?(v1, v2)
def ge?(%FastPass{time: v1}, %FastPass{time: v2}), do: Ord.ge?(v1, v2)
end
Run It
iex(9)> datetime_1 = DateTime.new!(~D[2025-06-01], ~T[13:10:00.000005])
~U[2025-06-01 13:10:00.000005Z]
iex(10)> datetime_2 = DateTime.new!(~D[2025-06-01], ~T[13:40:00.000004])
~U[2025-06-01 13:40:00.000004Z]
iex(11)> datetime_1 < datetime_2
false
iex(12)> DateTime.compare(datetime_1, datetime_2)
:lt
Okay something to go over here is that any compare with datetime is starting with the millisecond and as such doesn’t work as well as you would think right off the bat. DateTime.Compare/2 works in a better way and will return (:lt, :gt, :eq) so you can then test if it is the right order. Let’s use that now in the impl. This will need to be an other implementation for the ord.ex.
defimpl FunPark.Ord, for: DateTime do
def lt?(a, b), do: DateTime.compare(a, b) == :lt
def le?(a, b), do: match?(x when x in [:lt, :eq], DateTime.compare(a, b))
def gt?(a, b), do: DateTime.compare(a, b) == :gt
def ge?(a, b), do: match?(x when x in [:gt, :eq], DateTime.compare(a, b))
end
Run It
iex(13)> fast_pass_1 = FunPark.FastPass.make(banana_slip, datetime_1)
%FunPark.FastPass{
id: 3522,
ride: %FunPark.Ride{
id: 3138,
name: "Banana Slip",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
},
time: ~U[2025-06-01 13:10:00.000005Z]
}
iex(14)> fast_pass_2 = FunPark.FastPass.make(apple_cart, datetime_2)
%FunPark.FastPass{
id: 3586,
ride: %FunPark.Ride{
id: 3202,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
},
time: ~U[2025-06-01 13:40:00.000004Z]
}
iex(15)> FunPark.Ord.lt?(fast_pass_1, fast_pass_2)
true
iex(16)> time_3 = DateTime.new!(~D[2025-06-01], ~T[15:00:00.000012])
~U[2025-06-01 15:00:00.000012Z]
iex(18)> fast_pass_1 = FunPark.FastPass.change(fast_pass_1, %{time: time_3})
%FunPark.FastPass{
id: 3522,
ride: %FunPark.Ride{
id: 3138,
name: "Banana Slip",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
},
time: ~U[2025-06-01 15:00:00.000012Z]
}
iex(19)> FunPark.Ord.gt?(fast_pass_1, fast_pass_2)
true
Patron Context
Okay so for the basic version of the code the name will work just find to define a Patron order. But we might want to define them based of the ticket_tier. Let’s the make a new contramap/1 for this purpose
Transform Inputs Before Comparison
defmodule FunPark.Ord.Utils do
alias FunPark.Ord
def contramap(f, ord \\ Ord) do
ord = to_ord_map(ord)
%{
lt?: fn a, b -> ord.lt?.(f.(a), f.(b)) end,
le?: fn a, b -> ord.le?.(f.(a), f.(b)) end,
gt?: fn a, b -> ord.gt?.(f.(a), f.(b)) end,
ge?: fn a, b -> ord.ge?.(f.(a), f.(b)) end
}
end
def to_ord_map(%{lt?: lt_f, le?: le_f, gt?: gt_f, ge?: ge_f} = ord_map)
when is_function(lt_f, 2) and
is_function(le_f, 2) and
is_function(gt_f, 2) and
is_function(ge_f, 2),
do: ord_map
def to_ord_map(module) when is_atom(module) do
%{
lt?: &module.lt?/2,
le?: &module.le?/2,
gt?: &module.gt?/2,
ge?: &module.ge?/2
}
end
end
Again to go over this we are setting up the contramap for the Ord.Utils that will run when we want to change the data type of the data we are setting as the params. Once we run that we and then send in a function/module that we want to use to transform the data. We are also setting a transformer that will make sure that the data will always be a map that we can use.
Now we need to leverage this with an ordering system for the tickets so let’s head back to the Patron context.
defp tier_priority(:vip), do: 3
defp tier_priority(:premium), do: 2
defp tier_priority(:basic), do: 1
defp tier_priority(_), do: 0
defp get_ticket_tier_priority(%__MODULE__{ticket_tier: ticket_tier}),
do: tier_priority(ticket_tier)
def ord_by_ticket_tier do
Ord.Utils.contramap(&get_ticket_tier_priority/1)
end
...
defimpl FunPark.Ord, for: FunPark.Patron do
alias FunPark.Ord
alias FunPark.Patron
def lt?(%FunPark.Patron{name: v1}, %FunPark.Patron{name: v2}),
do: Ord.lt?(v1, v2)
def le?(%FunPark.Patron{name: v1}, %FunPark.Patron{name: v2}),
do: Ord.le?(v1, v2)
def gt?(%FunPark.Patron{name: v1}, %FunPark.Patron{name: v2}),
do: Ord.gt?(v1, v2)
def ge?(%FunPark.Patron{name: v1}, %FunPark.Patron{name: v2}),
do: Ord.ge?(v1, v2)
end
This will take the current ticket_tier and turn that value into a int that we can compare.
Run It
iex(20)> alice = FunPark.Patron.make("Alice", 15, 50, ticket_tier: :premium)
%FunPark.Patron{
id: 3778,
name: "Alice",
age: 15,
height: 50,
ticket_tier: :premium,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(21)> beth = FunPark.Patron.make("Beth", 16, 53)
%FunPark.Patron{
id: 3842,
name: "Beth",
age: 16,
height: 53,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(22)> ticket_ord = FunPark.Patron.ord_by_ticket_tier()
%{
lt?: #Function<1.94864313/2 in FunPark.Ord.Utils.contramap/2>,
ge?: #Function<4.94864313/2 in FunPark.Ord.Utils.contramap/2>,
gt?: #Function<3.94864313/2 in FunPark.Ord.Utils.contramap/2>,
le?: #Function<2.94864313/2 in FunPark.Ord.Utils.contramap/2>
}
iex(23)> ticket_ord.gt?.(alice, beth)
true
iex(24)> beth = FunPark.Patron.change(beth, %{ticket_tier: :vip})
%FunPark.Patron{
id: 3842,
name: "Beth",
age: 16,
height: 53,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(25)> ticket_ord.gt?.(beth, alice
...(25)> )
true
See Act on It
Harness Order for Collections
OKay so now that we have that out of the way let’s start to work with ordering.
Single-Purpose Sorting
### FunPark.List
def sort_rides(rides) do
Enum.sort(rides, fn ride1, ride2 -> ride1.name < ride2.name end)
end
Simple sort that we can test now.
iex(42)> FunPark.List.sort_rides([banana_slip, apple_cart])
[
%FunPark.Ride{
id: 3202,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
},
%FunPark.Ride{
id: 3138,
name: "Banana Slip",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
]
Generalize the Sort
Now we can use the Ord in order to generalize the sort. Let’s start with a compare/2.
# FunPark.Ord.Utils
def compare(a, b, ord \\ Ord) do
ord = to_ord_map(ord)
cond do
ord.lt?.(a, b) -> :lt
ord.gt?.(a, b) -> :gt
true -> :eq
end
end
We can test this to make sure it works.
iex(43)> FunPark.Ord.Utils.compare(1, 1)
:eq
iex(44)> FunPark.Ord.Utils.compare(1, 2)
:lt
iex(45)> FunPark.Ord.Utils.compare(1, 0)
:gt
iex(46)> FunPark.Ord.Utils.compare(apple_cart, apple_cart)
:eq
iex(47)> FunPark.Ord.Utils.compare(apple_cart, banana_slip)
:lt
iex(48)> FunPark.Ord.Utils.compare(banana_slip, apple_cart)
:gt
Okay so we have the compare that will leverage the ord.lt? and ord.gt? but we need to add in a comparator/1 that will be our function that will use the compare on 2 values.
# FunPark.Ord.Utils
def comparator(ord_module) do
fn a, b -> compare(a, b, ord_module) != :gt end
end
Now we can add in the full sort to the list.ex
def sort(list, ord \\ FunPark.Ord) when is_list(list) do
Enum.sort(list, Ord.Utils.comparator(ord))
end
Okay to go over this we created a compare that will leverage the Ord in order to test the values that we have set before. Then we used that to make a single function that will use compare/2, lastly we used the built-in elixir Enum.sort/2 in order to return a sorted list of data.
iex(49)> FunPark.List.sort([:banana, :pear, :apple])
[:apple, :banana, :pear]
iex(50)> FunPark.List.sort([banana_slip, apple_cart])
[
%FunPark.Ride{
id: 3202,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
},
%FunPark.Ride{
id: 3138,
name: "Banana Slip",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
]
Strict Sort
So for this we want to be able to return a list of rides based off of the wait times. We don’t need to see and exhausting list just the unique values. We could do that with unique/2 and sort/2
Houston, We Have a Problem
We will need to be able to sort and uniq based off the same values, but in our current case we are unable to do that because eq? is based of of ID and ord is based off of wait time. In order to be able to do this we need to find a way to normalize the data.
# FunPark.Ord.Utils
def to_eq(ord \\ Ord) do
%{
eq?: fn a, b -> compare(a, b, ord) == :eq end,
not_eq?: fn a, b -> compare(a, b, ord) != :eq end
}
end
Now we can add a new function to the List.ex so we can strict sort.
# FunPark.List
def strict_sort(list, ord \\ FunPark.Ord) when is_list(list) do
list
|> uniq(Ord.Utils.to_eq(ord))
|> sort(ord)
end
Let’s do some testing
iex(51)> FunPark.List.strict_sort([:banana, :orange, :banana, :apple])
[:apple, :banana, :orange]
iex(52)> tea_cup = FunPark.Ride.make("Tea Cup", wait_time: 40)
%FunPark.Ride{
id: 14,
name: "Tea Cup",
min_age: 0,
min_height: 0,
wait_time: 40,
online: true,
tags: []
}
iex(53)> haunted_mansion = FunPark.Ride.make("Haunted Mansion", wait_time: 20)
%FunPark.Ride{
id: 78,
name: "Haunted Mansion",
min_age: 0,
min_height: 0,
wait_time: 20,
online: true,
tags: []
}
iex(55)> river_ride = FunPark.Ride.make("River Ride", wait_time: 40)
%FunPark.Ride{
id: 206,
name: "River Ride",
min_age: 0,
min_height: 0,
wait_time: 40,
online: true,
tags: []
}
iex(56)> rides = [tea_cup, haunted_mansion, river_ride]
[
%FunPark.Ride{
id: 14,
name: "Tea Cup",
min_age: 0,
min_height: 0,
wait_time: 40,
online: true,
tags: []
},
%FunPark.Ride{
id: 78,
name: "Haunted Mansion",
min_age: 0,
min_height: 0,
wait_time: 20,
online: true,
tags: []
},
%FunPark.Ride{
id: 206,
name: "River Ride",
min_age: 0,
min_height: 0,
wait_time: 40,
online: true,
tags: []
}
]
iex(57)> ord_wait_time = FunPark.Ride.ord_by_wait_time()
%{
lt?: #Function<1.94864313/2 in FunPark.Ord.Utils.contramap/2>,
ge?: #Function<4.94864313/2 in FunPark.Ord.Utils.contramap/2>,
gt?: #Function<3.94864313/2 in FunPark.Ord.Utils.contramap/2>,
le?: #Function<2.94864313/2 in FunPark.Ord.Utils.contramap/2>
}
iex(58)> FunPark.List.sort(rides, ord_wait_time)
[
%FunPark.Ride{
id: 78,
name: "Haunted Mansion",
min_age: 0,
min_height: 0,
wait_time: 20,
online: true,
tags: []
},
%FunPark.Ride{
id: 14,
name: "Tea Cup",
min_age: 0,
min_height: 0,
wait_time: 40,
online: true,
tags: []
},
%FunPark.Ride{
id: 206,
name: "River Ride",
min_age: 0,
min_height: 0,
wait_time: 40,
online: true,
tags: []
}
]
Reverse the Order
What about when we want to reverse the order. We can make a new function that will use the opposite compare when called.
#FunPark.Ord.Utils
def reverse(ord \\ Ord) do
ord = to_ord_map(ord)
%{
lt?: ord.gt?,
le?: ord.ge?,
gt?: ord.lt?,
ge?: ord.le?
}
end
Run It
iex(60)> FunPark.Ord.Utils.compare(:apple, :banana)
:lt
iex(61)> FunPark.Ord.Utils.compare(:apple, :banana, reverse_ord)
:gt
iex(62)> alice = FunPark.Patron.make("Alice", 14, 140, ticket_tier: :vip)
%FunPark.Patron{
id: 270,
name: "Alice",
age: 14,
height: 140,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(63)> beth = FunPark.Patron.make("Beth", 15, 130, ticket_tier: :premium)
%FunPark.Patron{
id: 334,
name: "Beth",
age: 15,
height: 130,
ticket_tier: :premium,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(64)> FunPark.List.sort([alice, beth])
[
%FunPark.Patron{
id: 270,
name: "Alice",
age: 14,
height: 140,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
},
%FunPark.Patron{
id: 334,
name: "Beth",
age: 15,
height: 130,
ticket_tier: :premium,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
]
iex(66)> reverse_ord = FunPark.Ord.Utils.reverse()
%{
lt?: &FunPark.Ord.gt?/2,
ge?: &FunPark.Ord.le?/2,
gt?: &FunPark.Ord.lt?/2,
le?: &FunPark.Ord.ge?/2
}
iex(67)> FunPark.List.sort([alice, beth], reverse_ord)
[
%FunPark.Patron{
id: 334,
name: "Beth",
age: 15,
height: 130,
ticket_tier: :premium,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
},
%FunPark.Patron{
id: 270,
name: "Alice",
age: 14,
height: 140,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
]
So you can see here that we are using the original sort/1 to get a normal list but then we are sending that into the reverse/1. You can even set a unique sort and then run it through the reverse and then set that as an anonymous function to send to the sort.
Reduce Repetitive Code with Macros
Much of what we have done has been pretty much boiler plate for the defimpl. So let’s add in some macros to inject some code into the different modules.
# FunPark.Macros
defmacro ord_for(for_struct, field) do
quote do
alias FunPark.Ord
defimpl FunPark.Ord, for: unquote(for_struct) do
def lt?(
%unquote(for_struct){unquote(field) => v1},
%unquote(for_struct){unquote(field) => v2}
),
do: Ord.lt?(v1, v2)
def le?(
%unquote(for_struct){unquote(field) => v1},
%unquote(for_struct){unquote(field) => v2}
),
do: Ord.le?(v1, v2)
def gt?(
%unquote(for_struct){unquote(field) => v1},
%unquote(for_struct){unquote(field) => v2}
),
do: Ord.gt?(v1, v2)
def ge?(
%unquote(for_struct){unquote(field) => v1},
%unquote(for_struct){unquote(field) => v2}
),
do: Ord.ge?(v1, v2)
end
end
end
Then we can just add that into the Patron context. Then we can get rid of the defimpl that we set.
ord_for(FunPark.Patron, :name)
What You’ve Learned
We have started with a simple compare that takes the built-in Elixir compares and then built off that. We then made the Ord.ex module to create the protocols. Once we had that set we took the time to and in the defimpl for the modules to leverage the protocols.
We then realized that we might want to have some other types of sort so we added in some different functions to the Ride and Patron context in order to to use the new Ord.
After that we wanted to add in some sort of Sort we started out with a basic Enum.Sort/2 then realized that we wanted to use it in many different places and with different sorts. We then created a basic compare/2 that used the Ord. Once that was done we created a comparator/1 that would take the compare/2 that we made. Once that was done we added in a sort/2 that utilized Enum.sort.
Lastly we added in an other function reverse/1 in order to replace the compare with a different reversed sort.
At the end we added in a macro for the Ord.