Manage Absence with Maybe
Mix.install([
{:fun_park,
git: "https://github.com/JKWA/funpark_notebooks.git",
branch: "main"
}
])
Advanced Functional Programming with Elixir
|
Interactive Examples from Chapter 8 Advanced Functional Programming with Elixir. |
Build the Structures
Just
```elixir
defmodule FunPark.Monad.Maybe.Just do
defstruct [:value]
def pure(nil), do: raise ArgumentError, "Just cannot contain nil"
def pure(value), do: %__MODULE__{value: value}
end
```
Nothing
```elixir
defmodule FunPark.Monad.Maybe.Nothing do
defstruct []
def pure(), do: %__MODULE__{}
end
```
Maybe
```elixir
defmodule FunPark.Monad.Maybe do
alias FunPark.Monad.Maybe.{Just, Nothing}
def just(value), do: Just.pure(value)
def nothing(), do: Nothing.pure()
def pure(value), do: Just.pure(value)
def just?(%Just{}), do: true
def just?(%Nothing{}), do: false
def nothing?(%Nothing{}), do: true
def nothing?(%Just{}), do: false
end
```
Let’s start with one value from each branch:
just_a = FunPark.Monad.Maybe.just("A")
nothing = FunPark.Monad.Maybe.nothing()
We can determine which branch we’re in using a refinement:
FunPark.Monad.Maybe.just?(just_a)
FunPark.Monad.Maybe.nothing?(just_a)
Fold Branches
```elixir
defimpl FunPark.Foldable, for: FunPark.Monad.Maybe.Just do
def fold_l(%FunPark.Monad.Maybe.Just{value: value}, just_fn, _nothing_fn),
do: just_fn.(value)
def fold_r(%FunPark.Monad.Maybe.Just{value: value}, just_fn, _nothing_fn),
do: just_fn.(value)
end
```
```elixir
defimpl FunPark.Foldable, for: FunPark.Monad.Maybe.Nothing do
def fold_l(%FunPark.Monad.Maybe.Nothing{}, _just_fn, nothing_fn), do: nothing_fn.()
def fold_r(%FunPark.Monad.Maybe.Nothing{}, _just_fn, nothing_fn), do: nothing_fn.()
end
```
Let’s return to our Ride
expert’s sensor problem:
good = FunPark.Monad.Maybe.just(10)
bad = FunPark.Monad.Maybe.nothing()
Then fold to extract the result:
FunPark.Foldable.fold_l(good, &"Sensor: #{&1}", fn -> "Broken" end)
FunPark.Foldable.fold_l(bad, &"Sensor: #{&1}", fn -> "Broken" end)
Default Value
```elixir
def get_or_else(maybe, default) do
Foldable.fold_l(maybe, fn x -> x end, fn -> default end)
end
```
Let’s start by generating some flaky sensors:
sensor_a = FunPark.Monad.Maybe.pure(20)
sensor_b = FunPark.Monad.Maybe.nothing()
sensor_c = FunPark.Monad.Maybe.pure(30)
sensor_d = FunPark.Monad.Maybe.pure(5)
Now we can use the higher-level get_or_else/2
:
get_or_else_5 = &FunPark.Monad.Maybe.get_or_else(&1, 5)
Here, get_or_else_5/1
folds Maybe
a number into a number:
get_or_else_5.(sensor_a)
|> FunPark.Math.sum(get_or_else_5.(sensor_b))
|> FunPark.Math.sum(get_or_else_5.(sensor_c))
|> FunPark.Math.sum(get_or_else_5.(sensor_d))
Lift Other Contexts
Identity
```elixir
def lift_identity(%Identity{value: nil}), do: nothing()
def lift_identity(%Identity{value: value}), do: just(value)
```
Let’s say we’re dealing with a person, whose name might be missing (nil
):
person_1 = FunPark.Identity.pure("Dave")
person_2 = FunPark.Identity.pure(nil)
Our lift_identity/1
logic converts this to a Maybe
:
maybe_person_1 = FunPark.Monad.Maybe.lift_identity(person_1)
maybe_person_2 = FunPark.Monad.Maybe.lift_identity(person_2)
And we can fold that Maybe
back into a string:
FunPark.Monad.Maybe.get_or_else(maybe_person_1, "Missing")
FunPark.Monad.Maybe.get_or_else(maybe_person_2, "Missing")
Predicate
```elixir
def lift_predicate(value, predicate) do
Foldable.fold_l(fn -> predicate.(value) end, fn -> just(value) end, fn -> nothing() end)
end
```
Let’s start by generating the Tea Cup ride with a wait time of 100 minutes:
tea_cup = FunPark.Ride.make("Tea Cup", online: true, wait_time: 100)
Earlier, our Ride
expert introduced suggested?/1
, a predicate that checks whether a ride is online and has a wait time under thirty minutes. With a 100-minute wait, the Tea Cup is not suggested:
FunPark.Ride.suggested?(tea_cup)
Because the Tea Cup’s wait time is too long, it becomes nothing in the new context:
FunPark.Monad.Maybe.lift_predicate(tea_cup, &FunPark.Ride.suggested?/1)
Later, the wait time drops:
tea_cup = FunPark.Ride.update_wait_time(tea_cup, 10)
Now, the Tea Cup is just a SuggestedRide
:
FunPark.Monad.Maybe.lift_predicate(tea_cup, &FunPark.Ride.suggested?/1)
Bridge Elixir Patterns
```elixir
def from_nil(nil), do: nothing()
def from_nil(value), do: just(value)
def to_nil(%Just{value: value}), do: value
def to_nil(%Nothing{}), do: nil
```
```elixir
def get_fast_pass(%Patron{} = patron, %__MODULE__{} = ride) do
patron
|> Patron.get_fast_passes()
|> Enum.find(&FastPass.valid?(&1, ride))
|> Maybe.from_nil()
end
```
Let’s start by creating a Ride
, FastPass
, and Patron
:
haunted_mansion = FunPark.Ride.make("Haunted Mansion", min_age: 14)
datetime = DateTime.new!(~D[2025-06-01], ~T[13:00:00])
fast_pass = FunPark.FastPass.make(haunted_mansion, datetime)
alice = FunPark.Patron.make("Alice", 13, 150)
Alice does not have a ValidFastPass
, so the result is nothing:
FunPark.Ride.get_fast_pass(alice, haunted_mansion)
If she collects a fast-pass for the Haunted Mansion:
alice = FunPark.Patron.add_fast_pass(alice, fast_pass)
She now has just a ValidFastPass
:
FunPark.Ride.get_fast_pass(alice, haunted_mansion)
Define Equality
```elixir
defimpl FunPark.Eq, for: FunPark.Monad.Maybe.Just do
alias FunPark.Eq
alias FunPark.Monad.Maybe.{Just, Nothing}
def eq?(%Just{value: v1}, %Just{value: v2}), do: Eq.eq?(v1, v2)
def eq?(%Just{}, %Nothing{}), do: false
def not_eq?(%Just{value: v1}, %Just{value: v2}), do: Eq.not_eq?(v1, v2)
def not_eq?(%Just{}, %Nothing{}), do: true
end
```
```elixir
defimpl FunPark.Eq, for: FunPark.Monad.Maybe.Nothing do
alias FunPark.Monad.Maybe.{Just, Nothing}
def eq?(%Nothing{}, %Nothing{}), do: true
def eq?(%Nothing{}, %Just{}), do: false
def not_eq?(%Nothing{}, %Nothing{}), do: false
def not_eq?(%Nothing{}, %Just{}), do: true
end
```
Let’s return to our Patron
context, creating two versions of Alice: the original, and a copy after she’s updated her ticket to VIP:
alice = FunPark.Patron.make("Alice", 15, 150)
alice_copy = FunPark.Patron.change(alice, %{ticket_tier: :vip})
Our Eq
correctly recognizes that both records are still Alice:
FunPark.Eq.Utils.eq?(alice, alice_copy)
And like the Identity
monad, lifting both versions into the MaybePatron
preserves the equality:
alice_maybe = FunPark.Monad.Maybe.pure(alice)
alice_copy_maybe = FunPark.Monad.Maybe.pure(alice_copy)
FunPark.Eq.Utils.eq?(alice_maybe, alice_copy_maybe)
However, refining the context to MaybeVipPatron
draws a meaningful distinction:
alice_maybe_vip = FunPark.Monad.Maybe.lift_predicate(
alice, &FunPark.Patron.vip?/1
)
alice_copy_maybe_vip = FunPark.Monad.Maybe.lift_predicate(
alice_copy, &FunPark.Patron.vip?/1
)
Within the context of MaybeVipPatron
, these two records are not equal:
FunPark.Eq.Utils.eq?(alice_maybe_vip, alice_copy_maybe_vip)
Establish Order
```elixir
defimpl FunPark.Ord, for: FunPark.Monad.Maybe.Just do
alias FunPark.Ord
alias FunPark.Monad.Maybe.{Just, Nothing}
def lt?(%Just{value: v1}, %Just{value: v2}), do: Ord.lt?(v1, v2)
def lt?(%Just{}, %Nothing{}), do: false
def le?(%Just{value: v1}, %Just{value: v2}), do: Ord.le?(v1, v2)
def le?(%Just{}, %Nothing{}), do: false
def gt?(%Just{value: v1}, %Just{value: v2}), do: Ord.gt?(v1, v2)
def gt?(%Just{}, %Nothing{}), do: true
def ge?(%Just{value: v1}, %Just{value: v2}), do: Ord.ge?(v1, v2)
def ge?(%Just{}, %Nothing{}), do: true
end
```
```elixir
defimpl FunPark.Ord, for: FunPark.Monad.Maybe.Nothing do
alias FunPark.Monad.Maybe.{Just, Nothing}
def lt?(%Nothing{}, %Nothing{}), do: false
def lt?(%Nothing{}, %Just{}), do: true
def le?(%Nothing{}, %Nothing{}), do: true
def le?(%Nothing{}, %Just{}), do: true
def gt?(%Nothing{}, %Nothing{}), do: false
def gt?(%Nothing{}, %Just{}), do: false
def ge?(%Nothing{}, %Nothing{}), do: true
def ge?(%Nothing{}, %Just{}), do: false
end
```
Let’s start by regenerating Alice and Beth:
alice = FunPark.Patron.make("Alice", 15, 150, ticket_tier: :vip)
beth = FunPark.Patron.make("Beth", 15, 150)
The default order for a Patron
is alphabetical by name, so Alice is less than Beth:
FunPark.Ord.Utils.compare(alice, beth)
And like Eq
, lifting the patrons into MaybePatron
preserves that order:
alice_m = FunPark.Monad.Maybe.pure(alice)
beth_m = FunPark.Monad.Maybe.pure(beth)
FunPark.Ord.Utils.compare(alice_m, beth_m)
Also like Eq
, refining the context to MaybeVipPatron
changes the behavior:
alice_vip =
FunPark.Monad.Maybe.lift_predicate(alice, &FunPark.Patron.vip?/1)
beth_vip =
FunPark.Monad.Maybe.lift_predicate(beth, &FunPark.Patron.vip?/1)
FunPark.Ord.Utils.compare(alice_vip, beth_vip)
Lift Custom Comparisons
```elixir
def lift_eq(custom_eq) do
custom_eq = Eq.Utils.to_eq_map(custom_eq)
%{
eq?: fn
%Just{value: v1}, %Just{value: v2} -> custom_eq.eq?.(v1, v2)
%Nothing{}, %Nothing{} -> true
_, _ -> false
end,
not_eq?: fn
%Just{value: v1}, %Just{value: v2} -> custom_eq.not_eq?.(v1, v2)
%Nothing{}, %Nothing{} -> false
_, _ -> true
end
}
end
```
```elixir
def lift_ord(custom_ord) do
custom_ord = Ord.Utils.to_ord_map(custom_ord)
%{
lt?: fn
%Just{value: v1}, %Just{value: v2} -> custom_ord.lt?.(v1, v2)
%Nothing{}, %Just{} -> true
_, _ -> false
end,
le?: fn
%Just{value: v1}, %Just{value: v2} -> custom_ord.le?.(v1, v2)
%Nothing{}, _ -> true
%Just{}, %Nothing{} -> false
end,
gt?: fn
%Just{value: v1}, %Just{value: v2} -> custom_ord.gt?.(v1, v2)
%Just{}, %Nothing{} -> true
_, _ -> false
end,
ge?: fn
%Just{value: v1}, %Just{value: v2} -> custom_ord.ge?.(v1, v2)
%Just{}, _ -> true
%Nothing{}, %Nothing{} -> true
end
}
end
```
Let’s return to Alice and Beth:
alice = FunPark.Patron.make("Alice", 15, 150, ticket_tier: :vip)
beth = FunPark.Patron.make("Beth", 15, 150)
We’ll compare patrons using ord_by_ticket_tier/0
, where Alice, as a VIP, ranks higher than Beth:
ord_by_ticket = FunPark.Patron.ord_by_ticket_tier()
FunPark.Ord.Utils.compare(alice, beth, ord_by_ticket)
Now, let’s lift both patrons into the Maybe
context:
alice_maybe = FunPark.Monad.Maybe.pure(alice)
beth_maybe = FunPark.Monad.Maybe.pure(beth)
To use the same custom logic, we also need to lift the comparator:
lifted_ord = FunPark.Monad.Maybe.lift_ord(ord_by_ticket)
Alice, with her VIP ticket is greater:
FunPark.Ord.Utils.compare(alice_maybe, beth_maybe, lifted_ord)
Model Absence in a Monoid
```elixir
def max_priority_monoid do
%FunPark.Monoid.Max{
value: priority_empty(),
ord: ord_by_priority()
}
end
```
```elixir
def max_priority_maybe_monoid do
%FunPark.Monoid.Max{
value: FunPark.Monad.Maybe.nothing(),
ord: FunPark.Monad.Maybe.lift_ord(ord_by_priority())
}
end
```
```elixir
def highest_priority_maybe(patrons) when is_list(patrons) do
patrons
|> Enum.map(&FunPark.Monad.Maybe.pure/1)
|> then(&FunPark.Monoid.Utils.m_concat(max_priority_maybe_monoid(), &1))
end
```
Start by creating three patrons:
alice = FunPark.Patron.make(
"Alice", 15, 120,
reward_points: 50, ticket_tier: :premium
)
beth = FunPark.Patron.make(
"Beth", 16, 130,
reward_points: 20, ticket_tier: :vip
)
charles = FunPark.Patron.make(
"Charles", 14, 135,
reward_points: 150, ticket_tier: :premium
)
Using highest_priority_maybe/1
, we determine who should go next:
FunPark.Patron.highest_priority_maybe([alice, beth, charles])
FunPark.Patron.highest_priority_maybe([alice, charles])
FunPark.Patron.highest_priority_maybe([alice])
And when the queue is empty, the result is nothing:
FunPark.Patron.highest_priority_maybe([])
Implement the Monadic Behaviors
```elixir
defimpl FunPark.Monad, for: FunPark.Monad.Maybe.Nothing do
def map(%FunPark.Monad.Maybe.Nothing{}, _function), do: %FunPark.Monad.Maybe.Nothing{}
def bind(%FunPark.Monad.Maybe.Nothing{}, _function), do: %FunPark.Monad.Maybe.Nothing{}
def ap(%FunPark.Monad.Maybe.Nothing{}, _value), do: %FunPark.Monad.Maybe.Nothing{}
end
```
```elixir
defimpl FunPark.Monad, for: FunPark.Monad.Maybe.Just do
alias FunPark.Monad.Maybe.{Just, Nothing}
def map(%Just{value: value}, function) do
%Just{value: function.(value)}
end
def bind(%Just{value: value}, function) do
function.(value)
end
def ap(%Just{value: function}, %Just{value: value}) do
%Just{value: function.(value)}
end
def ap(%Just{}, %Nothing{}) do
%Nothing{}
end
end
```
Functor Map
```elixir
def update_wait_time_maybe(%__MODULE__{} = ride, wait_time)
when is_number(wait_time) do
ride
|> Maybe.lift_predicate(&online?/1)
|> map(&update_wait_time(&1, wait_time))
end
```
First, let’s generate an offline Tea Cup ride:
tea_cup = FunPark.Ride.make("Tea Cup", online: false)
FunPark.Ride.update_wait_time_maybe(tea_cup, 20)
Later, when the ride comes back online:
tea_cup = FunPark.Ride.change(tea_cup, %{online: true})
Now, Tea Cup is just an OnlineRide
, so the transformation is applied:
FunPark.Ride.update_wait_time_maybe(tea_cup, 20)
Monad Bind
```elixir
def check_ride_eligibility(%Patron{} = patron, %__MODULE__{} = ride) do
Maybe.lift_predicate(patron, &eligible?(&1, ride))
end
```
```elixir
def check_fast_pass(%Patron{} = patron, %__MODULE__{} = ride) do
get_fast_pass(patron, ride)
|> map(fn _ -> patron end)
end
```
```elixir
def fast_pass_lane(%Patron{} = patron, %__MODULE__{} = ride) do
check_fast_pass = curry_r(&check_fast_pass/2)
patron
|> check_ride_eligibility(ride)
|> bind(check_fast_pass.(ride))
end
```
Let’s start by setting up a ride, a FastPass, and a couple of patrons:
haunted_mansion = FunPark.Ride.make("Haunted Mansion", min_age: 14)
datetime = DateTime.new!(~D[2025-06-01], ~T[13:00:00])
fast_pass = FunPark.FastPass.make(haunted_mansion, datetime)
alice = FunPark.Patron.make("Alice", 15, 150)
beth = FunPark.Patron.make("Beth", 13, 130)
Alice does not qualify for the fast lane:
FunPark.Ride.fast_pass_lane(alice, haunted_mansion)
Unless we give her a FastPass
:
alice = FunPark.Patron.add_fast_pass(alice, fast_pass)
FunPark.Ride.fast_pass_lane(alice, haunted_mansion)
Beth is too young for the Haunted Mansion, so even with a FastPass
, she does not qualify:
beth = FunPark.Patron.add_fast_pass(beth, fast_pass)
FunPark.Ride.fast_pass_lane(beth, haunted_mansion)
Recovery
```elixir
def or_else(%Just{} = just, _thunk), do: just
def or_else(%Nothing{}, thunk), do: thunk.()
```
```elixir
def check_vip_or_fast_pass(%Patron{} = patron, %__MODULE__{} = ride) do
Maybe.lift_predicate(patron, &Patron.vip?/1)
|> or_else(fn -> check_fast_pass(patron, ride) end)
end
```
```elixir
def fast_pass_lane(%Patron{} = patron, %__MODULE__{} = ride) do
check_vip_or_pass = curry_r(&check_vip_or_fast_pass/2).(ride)
patron
|> check_ride_eligibility(ride)
|> bind(check_vip_or_pass)
end
```
Let’s generate Charles:
charles = FunPark.Patron.make("Charles", 15, 145)
Although Charles is eligible for the Haunted Mansion, he has neither a FastPass nor a VIP:
FunPark.Ride.fast_pass_lane(charles, haunted_mansion)
Later, he upgrades to VIP:
charles = FunPark.Patron.change(charles, %{ticket_tier: :vip})
And now qualifies:
FunPark.Ride.fast_pass_lane(charles, haunted_mansion)
Refine Lists
Concat
```elixir
def concat(maybe_list) when is_list(maybe_list) do
maybe_list
|> Enum.reduce([], fn maybe, acc ->
Foldable.fold_l(maybe, fn value -> [value | acc] end, fn -> acc end)
end)
|> :lists.reverse()
end
```
```elixir
def only_fast_pass_lane_concat(patrons, %__MODULE__{} = ride)
when is_list(patrons) do
patrons
|> Enum.map(&fast_pass_lane(&1, ride))
|> Maybe.concat()
end
```
Let’s start by generating a ride, a fast pass, and some patrons:
haunted_mansion = FunPark.Ride.make("Haunted Mansion", min_age: 14)
datetime = DateTime.new!(~D[2025-06-01], ~T[13:00:00])
fast_pass = FunPark.FastPass.make(haunted_mansion, datetime)
alice = FunPark.Patron.make("Alice", 15, 150)
beth = FunPark.Patron.make("Beth", 13, 135)
charles = FunPark.Patron.make("Charles", 15, 145)
Next, we’ll give Alice and Beth fast passes, and upgrade Charles to VIP:
alice = FunPark.Patron.add_fast_pass(alice, fast_pass)
beth = FunPark.Patron.add_fast_pass(beth, fast_pass)
charles = FunPark.Patron.change(charles, %{ticket_tier: :vip})
Now, we can use only_fast_pass_lane/2
to refine our list:
patrons = [alice, beth, charles]
FunPark.Ride.only_fast_pass_lane_concat(patrons, haunted_mansion)
Concat Map
```elixir
def concat_map(list, func) when is_list(list) and is_function(func) do
list
|> Enum.reduce([], fn item, acc ->
case func.(item) do
%Just{value: value} -> [value | acc]
%Nothing{} -> acc
end
end)
|> :lists.reverse()
end
```
```elixir
def only_fast_pass_lane(patrons, %__MODULE__{} = ride)
when is_list(patrons) do
Maybe.concat_map(patrons, &fast_pass_lane(&1, ride))
end
```
Let’s return to our patrons and ride. The only_fast_pass_lane/2
function still returns just the eligible patrons:
patrons = [alice, beth, charles]
FunPark.Ride.only_fast_pass_lane(patrons, haunted_mansion)
Sequence
```elixir
def sequence([]), do: pure([])
def sequence([head | tail]) do
bind(head, fn value ->
bind(sequence(tail), fn rest ->
pure([value | rest])
end)
end)
end
```
```elixir
def group_fast_pass_lane(patrons, %__MODULE__{} = ride)
when is_list(patrons) do
patrons
|> Enum.map(&fast_pass_lane(&1, ride))
|> Maybe.sequence()
end
```
Let’s reuse our ride and patrons:
Remember, even though she has a fast-pass, Beth is too young, so the group cannot enter the fast lane:
patrons = [alice, beth, charles]
FunPark.Ride.group_fast_pass_lane(patrons, haunted_mansion)
However, if Beth steps aside, as a group, Alice and Charles can enter the fast lane:
FunPark.Ride.group_fast_pass_lane([alice, charles], haunted_mansion)
Traverse
```elixir
def traverse(list, func) when is_list(list) and is_function(func) do
Enum.reduce_while(list, pure([]), fn item, acc ->
case bind(acc, fn values ->
case func.(item) do
%Just{value: value} -> pure([value | values])
%Nothing{} = nothing -> nothing
end
end) do
%Just{} = result -> {:cont, result}
%Nothing{} = nothing -> {:halt, nothing}
end
end)
|> map(&:lists.reverse/1)
end
```
```elixir
def group_fast_pass_lane(patrons, %__MODULE__{} = ride)
when is_list(patrons) do
Maybe.traverse(patrons, &fast_pass_lane(&1, ride))
end
```
Returning to our patrons and ride, we get the same results—Beth is too young for the Haunted Mansion:
patrons = [alice, beth, charles]
FunPark.Ride.group_fast_pass_lane(patrons, haunted_mansion)
Filter Within Composition
```elixir
defprotocol FunPark.Filterable do
def guard(filterable, boolean)
def filter(filterable, predicate)
def filter_map(filterable, function)
end
```
```elixir
defimpl FunPark.Filterable, for: FunPark.Monad.Maybe.Nothing do
def guard(%FunPark.Monad.Maybe.Nothing{}, _boolean), do: %FunPark.Monad.Maybe.Nothing{}
def filter(%FunPark.Monad.Maybe.Nothing{}, _predicate), do: %FunPark.Monad.Maybe.Nothing{}
def filter_map(%FunPark.Monad.Maybe.Nothing{}, _function), do: %FunPark.Monad.Maybe.Nothing{}
end
```
```elixir
defimpl FunPark.Filterable, for: FunPark.Monad.Maybe.Just do
alias FunPark.Monad.Maybe.{Just, Nothing}
def guard(%Just{} = just, true), do: just
def guard(%Just{}, false), do: %Nothing{}
def filter(%Just{value: value} = just, predicate) do
if predicate.(value), do: just, else: %Nothing{}
end
def filter_map(%Just{value: value}, function) do
case function.(value) do
%Just{} = result -> result
%Nothing{} -> %Nothing{}
end
end
end
```
Guard
```elixir
def update_wait_time_maybe(%__MODULE__{} = ride, wait_time) do
ride
|> Maybe.lift_predicate(&online?/1)
|> map(&update_wait_time(&1, wait_time))
|> guard(wait_time >= 0)
end
```
Let’s start by generating a ride:
tea_cup = FunPark.Ride.make("Tea Cup")
Updating with a positive number and we get just the ride:
FunPark.Ride.update_wait_time_maybe(tea_cup, 10)
But if we provide a negative number, the guard short-circuits:
FunPark.Ride.update_wait_time_maybe(tea_cup, -10)
The pipeline continues to respect the OnlineRide
context. If we update the Tea Cup to be offline:
tea_cup = FunPark.Ride.change(tea_cup, %{online: false})
FunPark.Ride.update_wait_time_maybe(tea_cup, 10)
Filter
```elixir
def add_fast_pass_maybe(%__MODULE__{} = patron, %FastPass{} = fast_pass) do
ride = FastPass.get_ride(fast_pass)
patron
|> Maybe.pure()
|> filter(&Ride.eligible?(&1, ride))
|> map(&add_fast_pass(&1, fast_pass))
end
```