Home Posts Post Search Tag Search

Advanced Functional Elixir - 05 - Define Logic with Predicates
Published on: 2026-03-20 Tags: Blog, Side Project, Testing, Advanced Functional Programming, FunPark, Monoids, predicates

5. Define Logic with Predicates

The predicate is a statement that can be true or false within a context. In code it is a function that returns a Boolean.

Because predicates follow Boolean algebra, they compose:
• Conjunction (a ∧ b): True if both are true.
• Disjunction (a ∨ b): True if at least one is true.
• Negation (¬a): Inverts the result.

With all this said let’s work on defining some predicates for the FunPark.

Simple Predicates

# lib/fun_park/ride.ex
  def online?(%__MODULE__{online: online}), do: online

  def long_wait?(%__MODULE__{wait_time: wait_time}), do: wait_time > 30

Pretty simple to start let’s test it out

Run It

iex(1)> tea_cup = FunPark.Ride.make("Tea Cup", online: true, wait_time: 100)
%FunPark.Ride{
  id: 1731,
  name: "Tea Cup",
  min_age: 0,
  min_height: 0,
  wait_time: 100,
  online: true,
  tags: []
}
iex(2)> FunPark.Ride.online?(tea_cup)
true
iex(3)> FunPark.Ride.long_wait?(tea_cup)
true

Okay that works but we want to be able to test for a short_wait time as well. We don’t want to just add that we want to be for pragmatic. We will do this by creating a function that will do the opposite of the function passed.

# lib/fun_park/predicate.ex
defmodule FunPark.Predicate do
  import FunPark.Monoid.Utils, only: [m_append: 3, m_concat: 2]
  
  def p_not(pred) when is_function(pred) do
    fn value -> not pred.(value) end
  end
end

# lib/fun_park/ride.ex
  import FunPark.Predicate

  def short_wait?(%__MODULE__{}), do: p_not(&long_wait?/1)

Combine Predicates

Function Description
and Combines two predicates, returning true only if both return true.
or Combines two predicates, returning true if at least one returns true.
all Returns true only if every predicate in a set returns true.
any Returns true if at least one predicate in a set returns true.
none The inverse of any, returning true if none return true.

What is great about all that we have done so far is that we have made a lot of the monoids that we need.

ALL

Let’s start out by making one of the 2 that we need (Predicate.All, Predicate.Any)

defmodule FunPark.Monoid.Predicate.All do
  defstruct value: &FunPark.Monoid.Predicate.All.default_pred?/1

  def default_pred?(_), do: true
end


defimpl FunPark.Monoid, for: FunPark.Monoid.Predicate.All do
  alias FunPark.Monoid.Predicate.All

  def empty(_), do: %All{}

  def append(%All{} = p1, %All{} = p2) do
    %All{
      value: fn value -> p1.value.(value) and p2.value.(value) end
    }
  end

  def wrap(%All{}, value) when is_function(value, 1) do
    %All{value: value}
  end

  def unwrap(%All{value: value}), do: value
end

We now have a way to wrap, unwrap, append and then an empty.

Any

defmodule FunPark.Monoid.Predicate.Any do
  defstruct value: &FunPark.Monoid.Predicate.Any.default_pred?/1

  def default_pred?(_), do: false
end


defimpl FunPark.Monoid, for: FunPark.Monoid.Predicate.Any do
  alias FunPark.Monoid.Predicate.Any

  def empty(_), do: %Any{}

  def append(%Any{} = p1, %Any{} = p2) do
    %Any{
      value: fn value -> p1.value.(value) or p2.value.(value) end
    }
  end

  def wrap(%Any{}, value) when is_function(value, 1) do
    %Any{value: value}
  end

  def unwrap(%Any{value: value}), do: value
end

Now we can abstract away those modules into the Predicate

# lib/fun_park/predicate.ex
  def p_and(pred1, pred2) when is_function(pred1) and is_function(pred2) do
    m_append(%All{}, pred1, pred2)
  end


  def p_or(pred1, pred2) when is_function(pred1) and is_function(pred2) do
    m_append(%Any{}, pred1, pred2)
  end

  def p_all(p_list) when is_list(p_list) do
    m_concat(%All{}, p_list)
  end


  def p_any(p_list) when is_list(p_list) do
    m_concat(%Any{}, p_list)
  end


  def p_none(p_list) when is_list(p_list) do
    p_not(p_any(p_list))
  end

Okay so now we can create a suggested?/1 that will combine not long_wait? and online?.

# lib/fun_park/ride.
  def suggested?(%__MODULE__{} = ride),
    do: p_all([&online?/1, p_not(&long_wait?/1)]).(ride)

Run It

iex(5)> tea_cup = FunPark.Ride.make("Tea Cup", online: true, wait_time: 100)
%FunPark.Ride{
  id: 86,
  name: "Tea Cup",
  min_age: 0,
  min_height: 0,
  wait_time: 100,
  online: true,
  tags: []
}
iex(6)> FunPark.Ride.suggested?(tea_cup)
false
iex(7)> tea_cup = FunPark.Ride.change(tea_cup, %{wait_time: 10})
%FunPark.Ride{
  id: 86,
  name: "Tea Cup",
  min_age: 0,
  min_height: 0,
  wait_time: 10,
  online: true,
  tags: []
}
iex(8)> FunPark.Ride.suggested?(tea_cup)
true

Okay so we were able to create a ride that failed the check and then changed the ride wait time so that it now would get suggested.

Predicates That Span Contexts

So we have the Ride context that sets limits and then the Patron that will need to pass those limits in order to ride the ride. This forms a conformist relationship, where Patron conforms to the rules set by Ride. Let’s set some of that up within the Ride context.

# lib/fun_park/ride.ex
  def tall_enough?(%Patron{} = patron, %__MODULE__{min_height: min_height}),
    do: Patron.get_height(patron) >= min_height

  def old_enough?(%Patron{} = patron, %__MODULE__{min_age: min_age}),
    do: Patron.get_age(patron) >= min_age

  def eligible?(%Patron{} = patron, %__MODULE__{} = ride),
    do: p_all([&tall_enough?/2, &old_enough?/2]).(patron, ride)

Notice that we are not deconstructing the Patron within the function params. That way we are not relying no Patron staying static, what if we want to say height_inches later.

Run It

iex(10)> roller_mtn = FunPark.Ride.make(
...(10)> "Roller Mountain", min_height: 120, min_age: 12
...(10)> )
%FunPark.Ride{
  id: 214,
  name: "Roller Mountain",
  min_age: 12,
  min_height: 120,
  wait_time: 0,
  online: true,
  tags: []
}
iex(11)> alice = FunPark.Patron.make("Alice", 13, 119)
%FunPark.Patron{
  id: 278,
  name: "Alice",
  age: 13,
  height: 119,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(12)> alice |> FunPark.Ride.old_enough?(roller_mtn)
true
iex(13)> alice |> FunPark.Ride.tall_enough?(roller_mtn)
false
iex(14)> alice |> FunPark.Ride.eligible?(roller_mtn)
false
iex(15)> alice = FunPark.Patron.change(alice, %{height: 121})
%FunPark.Patron{
  id: 278,
  name: "Alice",
  age: 13,
  height: 121,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(16)> alice |> FunPark.Ride.eligible?(roller_mtn)
true

We created a ride that has some parameters, and alice didn’t met all of those. It worked for old_enough? but not tall_enough?, then she grew and now she can make it.

Compose Multi-Arity Functions with Curry

So right now we are able to leverage the p_all to add all the functional test into one function, but this only worked because we have the same arity for both functions there is an other way that we will work with that we allow for more robust versions of this.


We need to be able to test if a ride will be suggested? if it is open and not long_wait? as well as they are eligible?. That is were curry comes in and let’s build that now.

# lib/fun_park/utils.ex
defmodule FunPark.Utils do
  def curry(fun) when is_function(fun) do
    arity = :erlang.fun_info(fun, :arity) |> elem(1)
    curry(fun, arity, [])
  end

  defp curry(fun, 1, args),
    do: fn last_arg -> apply(fun, args ++ [last_arg]) end

  defp curry(fun, arity, args) when arity > 1 do
    fn next_arg -> curry(fun, arity - 1, args ++ [next_arg]) end
  end

The curry/1 function uses Erlang’s fun_info/2 to retrieve the function’s arity:
• If the function is already unary, it wraps it as-is.
• Otherwise, it applies arguments one at a time, recursively returning a
new function until all arguments are provided.

Okay let’s add that into the Ride context.

# lib/fun_park/ride.ex
  def eligible?(%Patron{} = patron, %__MODULE__{} = ride),
    do: p_all([&tall_enough?/2, &old_enough?/2]).(patron, ride)

  def suggested?(%Patron{} = patron, %__MODULE__{} = ride),
    do:
      p_all([
        &suggested?/1,
        curry(&eligible?/2).(patron)
      ]).(ride)

Not So Fast

Okay so this should work but in this case the eligible?/2 requires other functions to be of the same arity. So we now need to curry everything that you will need down the chain.

# lib/fun_park/ride.ex
  def eligible?(%Patron{} = patron, %__MODULE__{} = ride),
    do:
      p_all([
        curry(&tall_enough?/2).(patron),
        curry(&old_enough?/2).(patron)
      ]).(ride)

  def suggested?(%Patron{} = patron, %__MODULE__{} = ride),
    do:
      p_all([
        &suggested?/1,
        curry(&eligible?/2).(patron)
      ]).(ride)

Now it will work for everything down the line. This took me a minute to get in my head. I think so version of going over this will help.


So the process of curry is all about passing one argument at at time to a new set of functions. So if you had curry(add(2, 1)) it would look something like

curried = curry(&add/3)

curried.(1) 
#returns
fn b ->
  fn c -> 
    add(1, b, c)

curried.(1).(2)
# returns
fn c ->
  add(1, 2, c)

curried.(1).(2).(3)
#returns
add(1, 2, 3)

Run It

iex(17)> alice |> FunPark.Ride.suggested?(roller_mtn)
true
iex(18)> roller_mtn = FunPark.Ride.change(roller_mtn, %{online: false})
%FunPark.Ride{
  id: 214,
  name: "Roller Mountain",
  min_age: 12,
  min_height: 120,
  wait_time: 0,
  online: false,
  tags: []
}
iex(19)> alice |> FunPark.Ride.suggested?(roller_mtn)
false

The logic works here and can be used in the future.

Harness Predicates for Collections

Function Description Enum Function
Returns true if all elements satisfy the predicate Enum.all?/2
Returns true if at least one element satisfies the predicate Enum.any?/2
Counts elements that satisfy the predicate Enum.count/2
Drops elements from the beginning while the predicate returns true Enum.drop_while/2
Returns a list of elements that satisfy the predicate Enum.filter/2
Returns the first element that satisfies the predicate Enum.find/2
Returns the index of the first element that satisfies the predicate Enum.find_index/2
Returns a list of elements that do not satisfy the predicate Enum.reject/2
Takes elements from the beginning while the predicate returns true Enum.take_while/2
Splits a list at the first element where the predicate returns false Enum.split_while/2

So unlike Eq or Ord Enum integrates with predicates.

Run It

Let’s first create a list of rides that are online and offline

iex(20)> thunder_loop = FunPark.Ride.make("Thunder Loop")
%FunPark.Ride{
  id: 726,
  name: "Thunder Loop",
  min_age: 0,
  min_height: 0,
  wait_time: 0,
  online: true,
  tags: []
}
iex(21)> ghost_hollow = FunPark.Ride.make("Ghost Hollow", online: false)
%FunPark.Ride{
  id: 790,
  name: "Ghost Hollow",
  min_age: 0,
  min_height: 0,
  wait_time: 0,
  online: false,
  tags: []
}
iex(22)> rocket_ridge = FunPark.Ride.make("Rocket Ridge")
%FunPark.Ride{
  id: 854,
  name: "Rocket Ridge",
  min_age: 0,
  min_height: 0,
  wait_time: 0,
  online: true,
  tags: []
}
iex(23)> jungle_river = FunPark.Ride.make("Jungle River", online: false)
%FunPark.Ride{
  id: 918,
  name: "Jungle River",
  min_age: 0,
  min_height: 0,
  wait_time: 0,
  online: false,
  tags: []
}
iex(24)> nebula_falls = FunPark.Ride.make("Nebula Falls")
%FunPark.Ride{
  id: 982,
  name: "Nebula Falls",
  min_age: 0,
  min_height: 0,
  wait_time: 0,
  online: true,
  tags: []
}
iex(25)> timber_twister = FunPark.Ride.make("Timber Twister", online: false)
%FunPark.Ride{
  id: 1046,
  name: "Timber Twister",
  min_age: 0,
  min_height: 0,
  wait_time: 0,
  online: false,
  tags: []
}
iex(26)> rides = [
...(26)> thunder_loop,
...(26)> ghost_hollow,
...(26)> rocket_ridge,
...(26)> jungle_river,
...(26)> nebula_falls,
...(26)> timber_twister
...(26)> ]
[
...
]
iex(27)> online? = &FunPark.Ride.online?/1
&FunPark.Ride.online?/1

Predicate Checks

iex(30)> rides |> Enum.all?(online?)
false
iex(31)> rides |> Enum.any?(online?)
true

Counting

rides |> Enum.count(online?)

Finding Elements

iex(32)> rides |> Enum.find(online?)
%FunPark.Ride{
  id: 726,
  name: "Thunder Loop",
  min_age: 0,
  min_height: 0,
  wait_time: 0,
  online: true,
  tags: []
}
iex(33)> rides |> Enum.find_index(online?)
0

Filtering and Rejecting Elements

iex(34)> rides |> Enum.filter(online?)
[
  %FunPark.Ride{
    id: 726,
    name: "Thunder Loop",
    min_age: 0,
    min_height: 0,
    wait_time: 0,
    online: true,
    tags: []
  },
  %FunPark.Ride{
    id: 854,
    name: "Rocket Ridge",
    min_age: 0,
    min_height: 0,
    wait_time: 0,
    online: true,
    tags: []
  },
  %FunPark.Ride{
    id: 982,
    name: "Nebula Falls",
    min_age: 0,
    min_height: 0,
    wait_time: 0,
    online: true,
    tags: []
  }
]
iex(35)> rides |> Enum.reject(online?)
[
  %FunPark.Ride{
    id: 790,
    name: "Ghost Hollow",
    min_age: 0,
    min_height: 0,
    wait_time: 0,
    online: false,
    tags: []
  },
  %FunPark.Ride{
    id: 918,
    name: "Jungle River",
    min_age: 0,
    min_height: 0,
    wait_time: 0,
    online: false,
    tags: []
  },
  %FunPark.Ride{
    id: 1046,
    name: "Timber Twister",
    min_age: 0,
    min_height: 0,
    wait_time: 0,
    online: false,
    tags: []
  }
]

Taking and Dropping While

iex(36)> rides |> Enum.take_while(online?)
[
  %FunPark.Ride{
    id: 726,
    name: "Thunder Loop",
    min_age: 0,
    min_height: 0,
    wait_time: 0,
    online: true,
    tags: []
  }
]
iex(37)> rides |> Enum.drop_while(online?)
[
  %FunPark.Ride{
    id: 790,
    name: "Ghost Hollow",
    min_age: 0,
    min_height: 0,
    wait_time: 0,
    online: false,
    tags: []
  },
  %FunPark.Ride{
    id: 854,
    name: "Rocket Ridge",
    min_age: 0,
    min_height: 0,
    wait_time: 0,
    online: true,
    tags: []
  },
  %FunPark.Ride{
    id: 918,
    name: "Jungle River",
    min_age: 0,
    min_height: 0,
    wait_time: 0,
    online: false,
    tags: []
  },
  %FunPark.Ride{
    id: 982,
    name: "Nebula Falls",
    min_age: 0,
    min_height: 0,
    wait_time: 0,
    online: true,
    tags: []
  },
  %FunPark.Ride{
    id: 1046,
    name: "Timber Twister",
    min_age: 0,
    min_height: 0,
    wait_time: 0,
    online: false,
    tags: []
  }
]

Splitting a List

iex(38)> rides |> Enum.split_while(online?)
{[
   %FunPark.Ride{
     id: 726,
     name: "Thunder Loop",
     min_age: 0,
     min_height: 0,
     wait_time: 0,
     online: true,
     tags: []
   }
 ],
 [
   %FunPark.Ride{
     id: 790,
     name: "Ghost Hollow",
     min_age: 0,
     min_height: 0,
     wait_time: 0,
     online: false,
     tags: []
   },
   %FunPark.Ride{
     id: 854,
     name: "Rocket Ridge",
     min_age: 0,
     min_height: 0,
     wait_time: 0,
     online: true,
     tags: []
   },
   %FunPark.Ride{
     id: 918,
     name: "Jungle River",
     min_age: 0,
     min_height: 0,
     wait_time: 0,
     online: false,
     tags: []
   },
   %FunPark.Ride{
     id: 982,
     name: "Nebula Falls",
     min_age: 0,
     min_height: 0,
     wait_time: 0,
     online: true,
     tags: []
   },
   %FunPark.Ride{
     id: 1046,
     name: "Timber Twister",
     min_age: 0,
     min_height: 0,
     wait_time: 0,
     online: false,
     tags: []
   }
 ]}

Suggested Rides


Now we can add some of this logic to the Ride context. Let’s use filter to get all the rides that should be suggested?

  def suggested_rides(%Patron{} = patron, rides) when is_list(rides) do
    Enum.filter(rides, &suggested?(patron, &1))
  end

Run It

iex(39)> tea_cup = FunPark.Ride.make("Tea Cup")
%FunPark.Ride{
  id: 1814,
  name: "Tea Cup",
  min_age: 0,
  min_height: 0,
  wait_time: 0,
  online: true,
  tags: []
}
iex(40)> roller_mtn = FunPark.Ride.make("Roller Mountain", min_height: 120)
%FunPark.Ride{
  id: 1878,
  name: "Roller Mountain",
  min_age: 0,
  min_height: 120,
  wait_time: 0,
  online: true,
  tags: []
}
iex(41)> haunted_mansion = FunPark.Ride.make("Haunted Mansion", min_age: 14)
%FunPark.Ride{
  id: 1942,
  name: "Haunted Mansion",
  min_age: 14,
  min_height: 0,
  wait_time: 0,
  online: true,
  tags: []
}
iex(42)> rides = [tea_cup, roller_mtn, haunted_mansion]
[
  %FunPark.Ride{
    id: 1814,
    name: "Tea Cup",
    min_age: 0,
    min_height: 0,
    wait_time: 0,
    online: true,
    tags: []
  },
  %FunPark.Ride{
    id: 1878,
    name: "Roller Mountain",
    min_age: 0,
    min_height: 120,
    wait_time: 0,
    online: true,
    tags: []
  },
  %FunPark.Ride{
    id: 1942,
    name: "Haunted Mansion",
    min_age: 14,
    min_height: 0,
    wait_time: 0,
    online: true,
    tags: []
  }
]
iex(43)> alice = FunPark.Patron.make("Alice", 13, 150)
%FunPark.Patron{
  id: 2006,
  name: "Alice",
  age: 13,
  height: 150,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(44)> beth = FunPark.Patron.make("Beth", 15, 110)
%FunPark.Patron{
  id: 2070,
  name: "Beth",
  age: 15,
  height: 110,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(45)> alice |> FunPark.Ride.suggested_rides(rides)
[
  %FunPark.Ride{
    id: 1814,
    name: "Tea Cup",
    min_age: 0,
    min_height: 0,
    wait_time: 0,
    online: true,
    tags: []
  },
  %FunPark.Ride{
    id: 1878,
    name: "Roller Mountain",
    min_age: 0,
    min_height: 120,
    wait_time: 0,
    online: true,
    tags: []
  }
]
iex(46)> beth |> FunPark.Ride.suggested_rides(rides
...(46)> )
[
  %FunPark.Ride{
    id: 1814,
    name: "Tea Cup",
    min_age: 0,
    min_height: 0,
    wait_time: 0,
    online: true,
    tags: []
  },
  %FunPark.Ride{
    id: 1942,
    name: "Haunted Mansion",
    min_age: 14,
    min_height: 0,
    wait_time: 0,
    online: true,
    tags: []
  }
]
iex(47)> tea_cup = FunPark.Ride.change(tea_cup, %{wait_time: 40})
%FunPark.Ride{
  id: 1814,
  name: "Tea Cup",
  min_age: 0,
  min_height: 0,
  wait_time: 40,
  online: true,
  tags: []
}
iex(48)> rides = [tea_cup, roller_mtn, haunted_mansion]
[
  %FunPark.Ride{
    id: 1814,
    name: "Tea Cup",
    min_age: 0,
    min_height: 0,
    wait_time: 40,
    online: true,
    tags: []
  },
  %FunPark.Ride{
    id: 1878,
    name: "Roller Mountain",
    min_age: 0,
    min_height: 120,
    wait_time: 0,
    online: true,
    tags: []
  },
  %FunPark.Ride{
    id: 1942,
    name: "Haunted Mansion",
    min_age: 14,
    min_height: 0,
    wait_time: 0,
    online: true,
    tags: []
  }
]
iex(49)> beth |> FunPark.Ride.suggested_rides(rides)
[
  %FunPark.Ride{
    id: 1942,
    name: "Haunted Mansion",
    min_age: 14,
    min_height: 0,
    wait_time: 0,
    online: true,
    tags: []
  }
]

So we made some Rides and some Patrons then checked for suggestions. Once that was done we updated a wait time and tested again.

Model the FastPass

So now we want to talk about how goes a FastPass that will go across 3 context: Ride, Patron, and FastPass. Let’s use a predicate to define the domain.

FastPass Management in the Patron Context

# lib/fun_park/patron.ex
  def add_fast_pass(%__MODULE__{} = patron, fast_pass) do
    fast_passes = List.union([fast_pass], get_fast_passes(patron))

    change(patron, %{fast_passes: fast_passes})
  end

  def remove_fast_pass(%__MODULE__{} = patron, fast_pass) do
    fast_passes = List.difference(get_fast_passes(patron), [fast_pass])

    change(patron, %{fast_passes: fast_passes})
  end

So we have the function to add a FastPass to a Patron that is because the FastPass will issue the fast_pass and then we can use the List.union to add that to the Patron. remove_fast_pass/2 is just as easy with List.difference

Run It

iex(50)> tea_cup = FunPark.Ride.make("Tea Cup")
%FunPark.Ride{
  id: 2326,
  name: "Tea Cup",
  min_age: 0,
  min_height: 0,
  wait_time: 0,
  online: true,
  tags: []
}
iex(51)> datetime = DateTime.new!(~D[2025-06-01], ~T[13:00:00])
~U[2025-06-01 13:00:00Z]
iex(52)> fast_pass = FunPark.FastPass.make(tea_cup, datetime)
%FunPark.FastPass{
  id: 2518,
  ride: %FunPark.Ride{
    id: 2326,
    name: "Tea Cup",
    min_age: 0,
    min_height: 0,
    wait_time: 0,
    online: true,
    tags: []
  },
  time: ~U[2025-06-01 13:00:00Z]
}
iex(53)> alice = FunPark.Patron.make("Alice", 13, 150)
%FunPark.Patron{
  id: 2582,
  name: "Alice",
  age: 13,
  height: 150,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(54)> alice = FunPark.Patron.add_fast_pass(alice, fast_pass)
%FunPark.Patron{
  id: 2582,
  name: "Alice",
  age: 13,
  height: 150,
  ticket_tier: :basic,
  fast_passes: [
    %FunPark.FastPass{
      id: 2518,
      ride: %FunPark.Ride{
        id: 2326,
        name: "Tea Cup",
        min_age: 0,
        min_height: 0,
        wait_time: 0,
        online: true,
        tags: []
      },
      time: ~U[2025-06-01 13:00:00Z]
    }
  ],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(55)> alice = FunPark.Patron.remove_fast_pass(alice, fast_pass)
%FunPark.Patron{
  id: 2582,
  name: "Alice",
  age: 13,
  height: 150,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}

Made a Ride, Patron and a FastPass then added and then removed it.

Validity Rules in the FastPass Context

This is where we want to have the ability to check to see if the FastPass is valid.

  def get_ride(%__MODULE__{ride: ride}), do: ride

  def valid?(%__MODULE__{} = fast_pass, %Ride{} = ride) do
    Eq.Utils.eq?(get_ride(fast_pass), ride)
  end

Fast Lane Access

  def fast_pass?(%Patron{} = patron, %__MODULE__{} = ride) do
    patron
    |> Patron.get_fast_passes()
    |> Enum.any?(&FastPass.valid?(&1, ride))
  end

Run It

With everything that we did above everything is worrying about its own Context.

iex(1)> haunted_mansion = FunPark.Ride.make("Haunted Mansion", min_age: 14)
%FunPark.Ride{
  id: 17,
  name: "Haunted Mansion",
  min_age: 14,
  min_height: 0,
  wait_time: 0,
  online: true,
  tags: []
}
iex(2)> datetime = DateTime.new!(~D[2025-06-01], ~T[13:00:00])
~U[2025-06-01 13:00:00Z]
iex(4)> fast_pass = FunPark.FastPass.make(haunted_mansion, datetime)
%FunPark.FastPass{
  id: 82,
  ride: %FunPark.Ride{
    id: 17,
    name: "Haunted Mansion",
    min_age: 14,
    min_height: 0,
    wait_time: 0,
    online: true,
    tags: []
  },
  time: ~U[2025-06-01 13:00:00Z]
}
iex(5)> alice = FunPark.Patron.make("Alice", 13, 150)
%FunPark.Patron{
  id: 146,
  name: "Alice",
  age: 13,
  height: 150,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(6)> alice |> FunPark.Ride.fast_pass?(haunted_mansion)
false
iex(7)> alice = FunPark.Patron.add_fast_pass(alice, fast_pass)
%FunPark.Patron{
  id: 146,
  name: "Alice",
  age: 13,
  height: 150,
  ticket_tier: :basic,
  fast_passes: [
    %FunPark.FastPass{
      id: 82,
      ride: %FunPark.Ride{
        id: 17,
        name: "Haunted Mansion",
        min_age: 14,
        min_height: 0,
        wait_time: 0,
        online: true,
        tags: []
      },
      time: ~U[2025-06-01 13:00:00Z]
    }
  ],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(8)> alice |> FunPark.Ride.fast_pass?(haunted_mansion)
true

Okay so now that we have a all this we ran into an issue that people are getting valid fast passes but are getting turned away at the ride. They weren’t eligible. What about a VIP they don’t need FastPass. Let’s look into that logic.

# lib/fun_park/ride.ex  
  def fast_pass_lane?(%Patron{} = patron, %__MODULE__{} = ride) do
    has_fast_pass = curry(&fast_pass?/2).(patron)
    is_eligible = curry(&eligible?/2).(patron)
    p_all([has_fast_pass, is_eligible]).(ride)
  end

# lib/fun_park/patron.ex
  def vip?(%__MODULE__{ticket_tier: :vip}), do: true
  def vip?(%__MODULE__{}), do: false

Well, That Got Complicated

Okay so right now we have the curry working in one way, left to right. It will depend on the order that we set all the functions. We could build and other set of functions or even just reverse them all, but that makes it harder to do. Let’s create a new curry that can work in reverse order.

# lib/fun_park/utils.ex
  def curry_r(fun) when is_function(fun) do
    arity = :erlang.fun_info(fun, :arity) |> elem(1)
    curry_r(fun, arity, [])
  end

  defp curry_r(fun, 1, args),
    do: fn last_arg -> apply(fun, [last_arg | args]) end

  defp curry_r(fun, arity, args) when arity > 1 do
    fn next_arg -> curry_r(fun, arity - 1, [next_arg | args]) end
  end

# lib/fun_park/ride.ex
  def fast_pass_lane?(%Patron{} = patron, %__MODULE__{} = ride) do
    has_fast_pass = curry_r(&fast_pass?/2).(ride)
    is_eligible = curry_r(&eligible?/2).(ride)
    is_vip = &Patron.vip?/1

    p_all([is_eligible, p_any([is_vip, has_fast_pass])]).(patron)
  end

Run It

iex(10)> alice = FunPark.Patron.make("Alice", 13, 150)
%FunPark.Patron{
  id: 402,
  name: "Alice",
  age: 13,
  height: 150,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(11)> beth = FunPark.Patron.make("Beth", 15, 110)
%FunPark.Patron{
  id: 466,
  name: "Beth",
  age: 15,
  height: 110,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(12)> haunted_mansion = FunPark.Ride.make("Haunted Mansion", min_age: 14)
%FunPark.Ride{
  id: 530,
  name: "Haunted Mansion",
  min_age: 14,
  min_height: 0,
  wait_time: 0,
  online: true,
  tags: []
}
iex(13)> datetime = DateTime.new!(~D[2025-06-01], ~T[13:00:00])
~U[2025-06-01 13:00:00Z]
iex(14)> fast_pass = FunPark.FastPass.make(haunted_mansion, datetime)
%FunPark.FastPass{
  id: 722,
  ride: %FunPark.Ride{
    id: 530,
    name: "Haunted Mansion",
    min_age: 14,
    min_height: 0,
    wait_time: 0,
    online: true,
    tags: []
  },
  time: ~U[2025-06-01 13:00:00Z]
}
iex(15)> alice = FunPark.Patron.add_fast_pass(alice, fast_pass)
%FunPark.Patron{
  id: 402,
  name: "Alice",
  age: 13,
  height: 150,
  ticket_tier: :basic,
  fast_passes: [
    %FunPark.FastPass{
      id: 722,
      ride: %FunPark.Ride{
        id: 530,
        name: "Haunted Mansion",
        min_age: 14,
        min_height: 0,
        wait_time: 0,
        online: true,
        tags: []
      },
      time: ~U[2025-06-01 13:00:00Z]
    }
  ],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(16)> alice |> FunPark.Ride.fast_pass_lane?(haunted_mansion)
false
iex(17)> beth |> FunPark.Ride.fast_pass_lane?(haunted_mansion)
false
iex(18)> beth = FunPark.Patron.change(beth, %{ticket_tier: :vip})
%FunPark.Patron{
  id: 466,
  name: "Beth",
  age: 15,
  height: 110,
  ticket_tier: :vip,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(19)> beth |> FunPark.Ride.fast_pass_lane?(haunted_mansion)
true

Fold Conditional Logic

# lib/fun_park/predicate.ex
defimpl FunPark.Foldable, for: Function do
  def fold_l(predicate, true_func, false_func) do
    case predicate.() do
      true -> true_func.()
      false -> false_func.()
    end
  end

  def fold_r(predicate, true_func, false_func) do
    fold_l(predicate, true_func, false_func)
  end
end

Run It

iex(20)> tea_cup = FunPark.Ride.make("Tea Cup", online: true, wait_time: 100)
%FunPark.Ride{
  id: 978,
  name: "Tea Cup",
  min_age: 0,
  min_height: 0,
  wait_time: 100,
  online: true,
  tags: []
}
iex(21)> FunPark.Ride.suggested?(tea_cup)
false
iex(22)> yes_or_no = fn val, pred ->
...(22)> FunPark.Foldable.fold_l(fn ->
...(22)> pred.(val) end, fn -> "Yes" end, fn -> "No" end) end
#Function<41.39164016/2 in :erl_eval.expr/6>
iex(23)> yes_or_no.(tea_cup, &FunPark.Ride.suggested?/1)
"No"

What You’ve Learned

Predicates are all about a truthy value. This helps with closure and can make it so you can keep things more concise.


Using this and keeping things into a clear boundary you are able to make sure that things are able to be used again and allow for more requirements to be fulfilled.


Elixir has Enum that can deal with predicates right off the bat.