Home Posts Post Search Tag Search

Advanced Functional Elixir - 03 - Create Flexible Ordering with Protocols
Published on: 2026-03-06 Tags: elixir, Blog, Side Project, Advanced Functional Programming, FunPark, Domain Driven Design

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.