Home Posts Post Search Tag Search

Advanced Functional Elixir - 03 - Act on It
Published on: 2026-03-06 Tags: elixir, Blog, Side Project, Advanced Functional Programming, FunPark, Act on It

Act on It

Patron expert wants to set order based on reward points. Implement that.

  ### Patron.ex
  def get_reward_points(%__MODULE__{reward_points: reward_points}),
    do: reward_points

  def ord_by_reward_points do
    Ord.Utils.contramap(&get_reward_points/1)
  end

Now let’s test it

iex(26)> ord_points = FunPark.Patron.ord_by_reward_points()
%{
  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(27)> guest_1 = FunPark.Patron.make("jim", 13, 70)
%FunPark.Patron{
  id: 3970,
  name: "jim",
  age: 13,
  height: 70,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(28)> guest_1 = FunPark.Patron.change(guest_1, %{reward_points: 200})
%FunPark.Patron{
  id: 3970,
  name: "jim",
  age: 13,
  height: 70,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 200,
  likes: [],
  dislikes: []
}
iex(29)> guest_2 = FunPark.Patron.make("sal", 20, 50)
%FunPark.Patron{
  id: 4034,
  name: "sal",
  age: 20,
  height: 50,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(30)> guest_2 = FunPark.Patron.change(guest_2, %{reward_points: 300})
%FunPark.Patron{
  id: 4034,
  name: "sal",
  age: 20,
  height: 50,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 300,
  likes: [],
  dislikes: []
}
iex(33)> ord_points.gt?.(guest_1, guest_2)
false
iex(34)> ord_points.lt?.(guest_1, guest_2)
true

You can apply the same pattern to any field on the struct. the contramap/1 call simply wraps whatever extractor you give it and returns a map of comparison functions. Try swapping the accessor to see different orderings:

### more helpers in Patron.ex

  def get_name(%__MODULE__{name: name}), do: name
  def get_age(%__MODULE__{age: age}),   do: age

  def ord_by_name do
    Ord.Utils.contramap(&get_name/1)
  end

  def ord_by_age do
    Ord.Utils.contramap(&get_age/1)
  end

Now experiment in iex:

iex> alice = FunPark.Patron.make("Alice", 15, 50, ticket_tier: :premium)
iex> beth  = FunPark.Patron.make("Beth", 16, 53)

iex> name_ord = FunPark.Patron.ord_by_name()
iex> age_ord  = FunPark.Patron.ord_by_age()
iex> points_ord = FunPark.Patron.ord_by_reward_points()

iex> name_ord.lt?.(alice, beth)          # alphabetical: "Alice" < "Beth"
true
iex> age_ord.lt?.(alice, beth)           # 15 < 16
true
iex> points_ord.lt?.(alice, beth)        # 0 < 0 (initially equal)
false

iex> beth = FunPark.Patron.change(beth, %{reward_points: 100})
iex> points_ord.lt?.(alice, beth)        # now 0 < 100
true

Swapping the extractor function (get_name/1, get_age/1, get_reward_points/1, or any other field) gives you a completely different ordering without touching the core protocol at all. That’s the power of contramap/1!

Ride expert want to set an order for rides that will be based off of wait time.

  def get_wait_time(%__MODULE__{wait_time: wait_time}), do: wait_time

  def ord_by_wait_time do
    Ord.Utils.contramap(&get_wait_time/1)
  end

That is the same thing we just did now we can leverage that to test. So long as we implement the right anonymous function.

iex(35)> 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(37)> fast_ride = FunPark.Ride.make("fast ride") |> FunPark.Ride.change(%{wait_time: 30})
%FunPark.Ride{
  id: 4290,
  name: "fast ride",
  min_age: 0,
  min_height: 0,
  wait_time: 30,
  online: true,
  tags: []
}
iex(38)> slow_ride = FunPark.Ride.make("slow ride") |> FunPark.Ride.change(%{wait_time: 60})
%FunPark.Ride{
  id: 4482,
  name: "slow ride",
  min_age: 0,
  min_height: 0,
  wait_time: 60,
  online: true,
  tags: []
}
iex(39)> ord_wait_time.lt?.(slow_ride, fast_ride)
false
iex(40)> ord_wait_time.gt?.(slow_ride, fast_ride)
true

Using contramap/1 and reverse/1 how would you sort patrons with the most points first?

iex(68)> ord_points = FunPark.Patron.ord_by_reward_points()
%{
  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(69)> rev_ord_points = FunPark.Ord.Utils.reverse(ord_points)
%{
  lt?: #Function<3.94864313/2 in FunPark.Ord.Utils.contramap/2>,
  ge?: #Function<2.94864313/2 in FunPark.Ord.Utils.contramap/2>,
  gt?: #Function<1.94864313/2 in FunPark.Ord.Utils.contramap/2>,
  le?: #Function<4.94864313/2 in FunPark.Ord.Utils.contramap/2>
}
iex(70)> lowest = FunPark.Patron.make("low", 20, 30) |> FunPark.Patron.change(%{reward_points: 10})
%FunPark.Patron{
  id: 462,
  name: "low",
  age: 20,
  height: 30,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 10,
  likes: [],
  dislikes: []
}
iex(71)> highest = FunPark.Patron.make("high", 20, 30) |> FunPark.Patron.change(%{reward_points: 30})
%FunPark.Patron{
  id: 590,
  name: "high",
  age: 20,
  height: 30,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 30,
  likes: [],
  dislikes: []
}
iex(72)> FunPark.List.sort([lowest, highest], rev_ord_points)
[
  %FunPark.Patron{
    id: 590,
    name: "high",
    age: 20,
    height: 30,
    ticket_tier: :basic,
    fast_passes: [],
    reward_points: 30,
    likes: [],
    dislikes: []
  },
  %FunPark.Patron{
    id: 462,
    name: "low",
    age: 20,
    height: 30,
    ticket_tier: :basic,
    fast_passes: [],
    reward_points: 10,
    likes: [],
    dislikes: []
  }
]

For this I was able to leverage a few things to get this to work. First we needed to set the proper ordering for the key-value pairs and then choose which :atom to look for. Once that was done we used the List module to get the sort.

Implement in Ord min/2 and max/2 to return the values based on the Ord given

  # FunPark.Ord.Utils
  def max(a, b, ord \\ Ord) do
    case compare(a, b, ord) do
      :lt -> b
      _ -> a
    end
  end

  def min(a, b, ord \\ Ord) do
    case compare(a, b, ord) do
      :gt -> b
      _ -> a
    end
  end

Now let’s test.

iex(73)> FunPark.Ord.Utils.min(lowest, highest, rev_ord_points)
%FunPark.Patron{
  id: 590,
  name: "high",
  age: 20,
  height: 30,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 30,
  likes: [],
  dislikes: []
}
iex(74)> FunPark.Ord.Utils.min(lowest, highest, ord_points)
%FunPark.Patron{
  id: 462,
  name: "low",
  age: 20,
  height: 30,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 10,
  likes: [],
  dislikes: []
}
iex(75)> FunPark.Ord.Utils.max(lowest, highest, ord_points)
%FunPark.Patron{
  id: 590,
  name: "high",
  age: 20,
  height: 30,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 30,
  likes: [],
  dislikes: []
}
iex(76)> FunPark.Ord.Utils.max(lowest, highest, rev_ord_points)
%FunPark.Patron{
  id: 462,
  name: "low",
  age: 20,
  height: 30,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 10,
  likes: [],
  dislikes: []
}

We used the same Patrons as we just made.

Create a macro for Eq

  defmacro eq_for(for_struct, field) do
    quote do
      alias FunPark.Eq

      defimpl FunPark.Ord, for: unquote(for_struct) do
        def eq?(
            %unquote(for_struct){unquote(field) => v1},
            %unquote(for_struct){unquote(field) => v2}
          ),
          do: Eq.eq?(v1, v2)
        def not_eq?(
            %unquote(for_struct){unquote(field) => v1},
            %unquote(for_struct){unquote(field) => v2},
          ),
          do: Eq.not_eq?(v1, v2)
    end
  end

Now you can add this to any module and you can even inject the basic version of the code that you want. Ill give you the the injection and so you can comment out the defimpl within each Context.

  # FunPark.Ride
  import FunPark.Macros, only: [eq_for: 2, ord_for: 2]

  ...

  ord_for(FunPark.Ride, :name)
  eq_for(FunPark.Ride, :id)

  # FunPark.Patron
  import FunPark.Macros, only: [eq_for: 2, ord_for: 2]

  ...
  
  ord_for(FunPark.Patron, :name)
  eq_for(FunPark.Patron, :id)

  # FaunPark.FastPass
  import FunPark.Macros, only: [eq_for: 2, ord_for: 2]

  ...
  
  eq_for(FunPark.FastPass, :id)
  ord_for(FunPark.FastPass, :time)

Now we can test to make sure that they work.

iex(79)> FunPark.Eq.eq?(lowest, highest)
false
iex(80)> FunPark.Eq.eq?(lowest, lowest)
true
iex(81)> FunPark.Ord.lt?(lowest, lowest)
false
iex(83)> FunPark.Ord.lt?(highest, lowest)
true

iex(84)> FunPark.Ord.lt?(tea_cup, river_ride)
false
iex(85)> FunPark.Ord.gt?(tea_cup, river_ride)
true
iex(86)> FunPark.Eq.eq?(tea_cup, tea_cup)
true
iex(87)> FunPark.Eq.eq?(tea_cup, river_ride)
false

iex(89)> FunPark.Ord.lt?(fast_pass_1, fast_pass_2)
false
iex(90)> FunPark.Ord.gt?(fast_pass_1, fast_pass_2)
true
iex(91)> FunPark.Eq.eq?(fast_pass_1, fast_pass_2)
false
iex(92)> FunPark.Eq.eq?(fast_pass_1, fast_pass_1)
true