We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
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.