We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
8. Manage Absence with Maybe
Maybe is a quantum: not a value, but the possibility of one. Maybe extends to list, enabling loops over collections or quantized values.
Build the Structures
Maybe context has two structures. Just for the presence and Nothing for the absence.
Just
defmodule FunPark.Monad.Maybe.Just do
@enforce_keys [:value]
defstruct [:value]
def pure(nil), do: raise(ArgumentError, "Cannot wrap nil in a Just")
def pure(value), do: %__MODULE__{value: value}
end
This is the start of making the structure stay consistent and removing any way that we will continue if we don’t have the right structure.
Nothing
defmodule FunPark.Monad.Maybe.Nothing do
defstruct []
def pure, do: %__MODULE__{}
end
Maybe
defmodule FunPark.Monad.Maybe do
import FunPark.Monad, only: [bind: 2, map: 2]
import FunPark.Foldable, only: [fold_l: 3]
alias FunPark.Monad.Maybe.{Just, Nothing}
# alias FunPark.Monad.Either.{Left, Right}
alias FunPark.Eq
alias FunPark.Identity
alias FunPark.Ord
def just(value), do: Just.pure(value)
def nothing, do: Nothing.pure()
def pure(value), do: just(value)
def just?(%Just{}), do: true
def just?(_), do: false
def nothing?(%Nothing{}), do: true
def nothing?(_), do: false
def guard(maybe, true), do: maybe
def guard(_maybe, false), do: nothing()
def filter(maybe, predicate) do
bind(maybe, fn value ->
if predicate.(value) do
pure(value)
else
nothing()
end
end)
end
We have some helper functions that will allow us to know that we are on the right track. The guard/2 will come in handy later to allow us to keep stacks working. There is the just/1 and nothing/0 that will delegate to the pure function.
just/1 and nothing/1 are refinement predicates, used to check if we are in the _Just or Nothing branch.
Run It
iex(1)> just_a = FunPark.Monad.Maybe.just("A")
%FunPark.Monad.Maybe.Just{value: "A"}
iex(2)> nothing = FunPark.Monad.Maybe.nothing()
%FunPark.Monad.Maybe.Nothing{}
iex(3)> FunPark.Monad.Maybe.just?(just_a)
true
iex(4)> FunPark.Monad.Maybe.nothing?(just_a)
false
So we see that we are getting a true or false for the ? branches and the just will build a value into the Just struct.
Fold Branches
Maybe is quantum of possibility it doesn’t collapse unless observed. just will build the function and nothing will give us a fallback. Folding is the way to get the function to collapse.
defimpl FunPark.Foldable, for: FunPark.Monad.Maybe do
alias FunPark.Monad.Maybe.{Just, Nothing}
def fold_l(%Just{value: value}, just_fn, _nothing_fn),
do: just_fn.(value)
def fold_l(%Nothing{}, _just_fn, nothing_fn), do: nothing_fn.()
def fold_r(%Just{value: value}, just_fn, _nothing_fn),
do: just_fn.(value)
def fold_r(%Nothing{}, _just_fn, nothing_fn), do: nothing_fn.()
end
Not So Fast There, Bub
So for Elixir protocols operate on structs, but Maybe isn’t a struct, it’s a sum type made up of Just and Nothing. So we need to implement them with Foldable we need to define the behavior for each branch.
# just.ex
defimpl FunPark.Foldable, for: FunPark.Monad.Maybe.Just do
alias FunPark.Monad.Maybe.Just
def fold_l(%Just{value: value}, just_func, _nothing_func) do
just_func.(value)
end
def fold_r(%Just{} = just, just_func, nothing_func) do
fold_l(just, just_func, nothing_func)
end
end
This will allow us to apply the just_func to the value if passed or fold_l to the functions to keep the train going.
# nothing.ex
defimpl FunPark.Foldable, for: FunPark.Monad.Maybe.Nothing do
alias FunPark.Monad.Maybe.Nothing
def fold_l(%Nothing{}, _just_func, nothing_func) do
nothing_func.()
end
def fold_r(%Nothing{} = nothing, just_func, nothing_func) do
fold_l(nothing, just_func, nothing_func)
end
end
This is one of the outs of the system, if we get a Nothing struct we will just return the nothing.func().
Run It
iex(5)> good = FunPark.Monad.Maybe.just(10)
%FunPark.Monad.Maybe.Just{value: 10}
iex(6)> bad = FunPark.Monad.Maybe.nothing()
%FunPark.Monad.Maybe.Nothing{}
iex(7)> FunPark.Foldable.fold_l(good, &"Sensor: #{&1}", fn -> "Broken" end)
"Sensor: 10"
iex(8)> FunPark.Foldable.fold_l(bad, &"Sensor: #{&1}", fn -> "Broken" end)
"Broken"
Okay so we are taking a Just and a Nothing and then giving them functions to do if we get a “good” or “bad” value for the Maybe.
Default Value
Okay so the Ride expert wants to make sure that we have a default as some info is better than no info. In this case we want to have it default to 5min if there is no info. We can now create a get_or_else/2 function:
def get_or_else(maybe, default) do
fold_l(maybe, fn value -> value end, fn -> default end)
end
We now have a case to return a default in case we don’t have a real Just
Run It
iex(9)> sensor_a = FunPark.Monad.Maybe.pure(20)
%FunPark.Monad.Maybe.Just{value: 20}
iex(10)> sensor_b = FunPark.Monad.Maybe.nothing()
%FunPark.Monad.Maybe.Nothing{}
iex(11)> sensor_c = FunPark.Monad.Maybe.pure(30)
%FunPark.Monad.Maybe.Just{value: 30}
iex(12)> sensor_d = FunPark.Monad.Maybe.pure(5)
%FunPark.Monad.Maybe.Just{value: 5}
iex(13)> get_or_else_5 = &FunPark.Monad.Maybe.get_or_else(&1, 5)
#Function<42.39164016/1 in :erl_eval.expr/6>
iex(14)> get_or_else_5.(sensor_a)
20
iex(15)> |> FunPark.Math.sum(get_or_else_5.(sensor_b))
25
iex(16)> |> FunPark.Math.sum(get_or_else_5.(sensor_c))
55
iex(17)> |> FunPark.Math.sum(get_or_else_5.(sensor_d))
60
So when we got the results of sensor b we only added 5 instead of the broken branch. Let’s see if we tried to pass the same sensor without the get_or_else/2 instead a get_or_nothing/1
def get_or_nothing(maybe) do
fold_l(maybe, fn value -> value end, fn -> nothing() end)
end
iex> sensor_e = FunPark.Monad.Maybe.just(5)
%FunPark.Monad.Maybe.Just{value: 5}
iex> sensor_f = FunPark.Monad.Maybe.just(10)
%FunPark.Monad.Maybe.Just{value: 10}
iex> sensor_g = FunPark.Monad.Maybe.nothing()
%FunPark.Monad.Maybe.Nothing{}
iex> get_or_nothing = &FunPark.Monad.Maybe.get_or_nothing(&1)
&FunPark.Monad.Maybe.get_or_nothing/1
iex> get_or_nothing.(sensor_e)
5
iex> get_or_nothing.(sensor_f)
10
iex> get_or_nothing.(sensor_g)
%FunPark.Monad.Maybe.Nothing{}
Lift Other Contexts
Now we need a way to take values and turn them into the Just or Nothing.
Identity
def lift_identity(%Identity{} = identity) do
case identity do
%Identity{value: nil} -> nothing()
%Identity{value: value} -> just(value)
end
end
We now have a way to put a input values into an identity and then pull them out with Maybe.life_identity/1
Run It
iex(28)> person_1 = FunPark.Identity.pure("Dave")
%FunPark.Identity{value: "Dave"}
iex(29)> person_2 = FunPark.Identity.pure(nil)
%FunPark.Identity{value: nil}
iex(30)> maybe_person_1 = FunPark.Monad.Maybe.lift_identity(person_1)
%FunPark.Monad.Maybe.Just{value: "Dave"}
iex(31)> maybe_person_2 = FunPark.Monad.Maybe.lift_identity(person_2)
%FunPark.Monad.Maybe.Nothing{}
iex(32)> FunPark.Monad.Maybe.get_or_else(maybe_person_1, "Missing")
"Dave"
iex(33)> FunPark.Monad.Maybe.get_or_else(maybe_person_2, "Missing")
"Missing"
Great we were able to replace a nil with a default, but we don’t want to just do that. We want to design it out.
Predicate
Okay so we want to now make a MaybeVipPatron that only does anything if the Patron is a VIP. In this case we want to make it so we can send in a bit of data (a struct) and then give it a way to define a good or bad outcome. If it doesn’t pass we return Nothing.
def lift_predicate(value, predicate) when is_function(predicate, 1) do
fold_l(
fn -> predicate.(value) end,
fn -> just(value) end,
fn -> nothing() end
)
end
fold_l/3 here is taking a structure and saying if true do the first if false do the second.
Run It
iex(34)> tea_cup = FunPark.Ride.make("Tea Cup", online: true, wait_time: 100)
%FunPark.Ride{
id: 20,
name: "Tea Cup",
min_age: 0,
min_height: 0,
wait_time: 100,
online: true,
tags: []
}
iex(35)> FunPark.Ride.suggested?(tea_cup)
false
iex(36)> FunPark.Monad.Maybe.lift_predicate(tea_cup, &FunPark.Ride.suggested?/1)
%FunPark.Monad.Maybe.Nothing{}
iex(37)> tea_cup = FunPark.Ride.update_wait_time(tea_cup, 10)
%FunPark.Ride{
id: 20,
name: "Tea Cup",
min_age: 0,
min_height: 0,
wait_time: 10,
online: true,
tags: []
}
iex(38)> FunPark.Monad.Maybe.lift_predicate(tea_cup, &FunPark.Ride.suggested?/1)
%FunPark.Monad.Maybe.Just{
value: %FunPark.Ride{
id: 20,
name: "Tea Cup",
min_age: 0,
min_height: 0,
wait_time: 10,
online: true,
tags: []
}
}
Here is the wrong Domain being passed.
iex(39)> patron = FunPark.Patron.make("alice", 33, 40)
%FunPark.Patron{
id: 142,
name: "alice",
age: 33,
height: 40,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(40)> FunPark.Monad.Maybe.lift_predicate(patron, &FunPark.Ride.suggested?/1)
** (FunctionClauseError) no function clause matching in FunPark.Ride.suggested?/1
The following arguments were given to FunPark.Ride.suggested?/1:
# 1
%FunPark.Patron{
id: 142,
name: "alice",
age: 33,
height: 40,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
Attempted function clauses (showing 1 out of 1):
def suggested?(%FunPark.Ride{} = ride)
(fun_park 0.1.0) lib/fun_park/ride.ex:155: FunPark.Ride.suggested?/1
(fun_park 0.1.0) lib/fun_park/predicate.ex:46: FunPark.Foldable.Function.fold_l/3
iex:40: (file)
Bridge Elixir Patterns
Elixir has ways of dealing with missing information, (nil). We use interop functions to bridge the gaps.
# maybe.ex
def from_nil(nil), do: nothing()
def from_nil(value), do: just(value)
def to_nil(%Nothing{}), do: nil
def to_nil(%Just{value: value}), do: value
We got from a nil to Nothing or Just, or we take a Nothing and go to nil or a Just to a value. Let’s use this in the Ride domain to get a Maybe depending on whether we have a valid FastPass
def get_fast_pass(%Patron{} = patron, %__MODULE__{} = ride) do
Enum.find(
Patron.get_fast_passes(patron),
&FastPass.valid?(&1, ride)
)
|> Maybe.from_nil()
end
Once we get a nil or a FastPass we can then return a Maybe
Run It
iex(42)> haunted_mansion = FunPark.Ride.make("Haunted Mansion", min_age: 14)
%FunPark.Ride{
id: 270,
name: "Haunted Mansion",
min_age: 14,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
iex(43)> datetime = DateTime.new!(~D[2025-06-01], ~T[13:00:00])
~U[2025-06-01 13:00:00Z]
iex(44)> fast_pass = FunPark.FastPass.make(haunted_mansion, datetime)
%FunPark.FastPass{
id: 462,
ride: %FunPark.Ride{
id: 270,
name: "Haunted Mansion",
min_age: 14,
min_height: 0,
wait_time: 0,
online: true,
tags: []
},
time: ~U[2025-06-01 13:00:00Z]
}
iex(45)> alice = FunPark.Patron.make("Alice", 13, 150)
%FunPark.Patron{
id: 526,
name: "Alice",
age: 13,
height: 150,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(46)> FunPark.Ride.get_fast_pass(alice, haunted_mansion)
%FunPark.Monad.Maybe.Nothing{}
iex(47)> alice = FunPark.Patron.add_fast_pass(alice, fast_pass)
%FunPark.Patron{
id: 526,
name: "Alice",
age: 13,
height: 150,
ticket_tier: :basic,
fast_passes: [
%FunPark.FastPass{
id: 462,
ride: %FunPark.Ride{
id: 270,
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(48)> FunPark.Ride.get_fast_pass(alice, haunted_mansion)
%FunPark.Monad.Maybe.Just{
value: %FunPark.FastPass{
id: 462,
ride: %FunPark.Ride{
id: 270,
name: "Haunted Mansion",
min_age: 14,
min_height: 0,
wait_time: 0,
online: true,
tags: []
},
time: ~U[2025-06-01 13:00:00Z]
}
}
We now have the ability to work in the Maybe with Elixir built-in functions.
Define Equality
We can now work with the Eq with maybe as there is a way to determine the relationship, as we now have a behavior.
# just.ex
defimpl FunPark.Eq, for: FunPark.Monad.Maybe.Just do
alias FunPark.Monad.Maybe.{Just, Nothing}
alias FunPark.Eq
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: not Eq.eq?(v1, v2)
def not_eq?(%Just{}, %Nothing{}), do: true
end
• Just unwraps its value and defers comparison to the underlying Eq imple-
mentation.
• Just is never equal to Nothing.
# nothing.ex
defimpl FunPark.Eq, for: FunPark.Monad.Maybe.Nothing do
alias FunPark.Monad.Maybe.{Nothing, Just}
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
• Nothing is always equal to Nothing.
• Nothing is never equal to Just.
Run It
iex(49)> alice = FunPark.Patron.make("Alice", 15, 150)
%FunPark.Patron{
id: 590,
name: "Alice",
age: 15,
height: 150,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(50)> alice_copy = FunPark.Patron.change(alice, %{ticket_tier: :vip})
%FunPark.Patron{
id: 590,
name: "Alice",
age: 15,
height: 150,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(51)> FunPark.Eq.Utils.eq?(alice, alice_copy)
true
iex(52)> alice_maybe = FunPark.Monad.Maybe.pure(alice)
%FunPark.Monad.Maybe.Just{
value: %FunPark.Patron{
id: 590,
name: "Alice",
age: 15,
height: 150,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
}
iex(53)> alice_copy_maybe = FunPark.Monad.Maybe.pure(alice_copy)
%FunPark.Monad.Maybe.Just{
value: %FunPark.Patron{
id: 590,
name: "Alice",
age: 15,
height: 150,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
}
iex(54)> FunPark.Eq.Utils.eq?(alice_maybe, alice_copy_maybe)
true
iex(55)> alice_maybe_vip = FunPark.Monad.Maybe.lift_predicate(
...(55)> alice, &FunPark.Patron.vip?/1
...(55)> )
%FunPark.Monad.Maybe.Nothing{}
iex(56)> alice_copy_maybe_vip = FunPark.Monad.Maybe.lift_predicate(
...(56)> alice_copy, &FunPark.Patron.vip?/1
...(56)> )
%FunPark.Monad.Maybe.Just{
value: %FunPark.Patron{
id: 590,
name: "Alice",
age: 15,
height: 150,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
}
iex(57)> FunPark.Eq.Utils.eq?(alice_maybe_vip, alice_copy_maybe_vip)
false
So we have the implementation for the Eq context within the new Maybe modules.
Establish Order
# just.ex
defimpl FunPark.Ord, for: FunPark.Monad.Maybe.Just do
alias FunPark.Monad.Maybe.{Just, Nothing}
alias FunPark.Ord
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
• Just unwraps its value and defers comparison to the underlying Ord
implementation.
• Just is always greater than Nothing.
# nothing.ex
defimpl FunPark.Ord, for: FunPark.Monad.Maybe.Nothing do
alias FunPark.Monad.Maybe.{Nothing, Just}
def lt?(%Nothing{}, %Just{}), do: true
def lt?(%Nothing{}, %Nothing{}), do: false
def le?(%Nothing{}, %Just{}), do: true
def le?(%Nothing{}, %Nothing{}), do: true
def gt?(%Nothing{}, %Just{}), do: false
def gt?(%Nothing{}, %Nothing{}), do: false
def ge?(%Nothing{}, %Just{}), do: false
def ge?(%Nothing{}, %Nothing{}), do: true
end
• Nothing is always equal to Nothing.
• Nothing is always less than Just.
Run It
iex(72)> alice=FunPark.Patron.make("Alice",15,150,ticket_tier: :vip)
%FunPark.Patron{
id: 782,
name: "Alice",
age: 15,
height: 150,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(73)> beth=FunPark.Patron.make("Beth",15,150)
%FunPark.Patron{
id: 846,
name: "Beth",
age: 15,
height: 150,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(74)> FunPark.Ord.Utils.compare(alice,beth)
:lt
iex(75)> alice_m=FunPark.Monad.Maybe.pure(alice)
%FunPark.Monad.Maybe.Just{
value: %FunPark.Patron{
id: 782,
name: "Alice",
age: 15,
height: 150,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
}
iex(76)> beth_m=FunPark.Monad.Maybe.pure(beth)
%FunPark.Monad.Maybe.Just{
value: %FunPark.Patron{
id: 846,
name: "Beth",
age: 15,
height: 150,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
}
iex(77)> FunPark.Ord.Utils.compare(alice_m,beth_m)
:lt
iex(78)> alice_vip=FunPark.Monad.Maybe.lift_predicate(alice,&FunPark.Patron.vip?/1)
%FunPark.Monad.Maybe.Just{
value: %FunPark.Patron{
id: 782,
name: "Alice",
age: 15,
height: 150,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
}
iex(79)> beth_vip=FunPark.Monad.Maybe.lift_predicate(beth,&FunPark.Patron.vip?/1)
%FunPark.Monad.Maybe.Nothing{}
iex(80)> FunPark.Ord.Utils.compare(alice_vip,beth_vip)
:gt
This works because we now have a value for the Nothing so that it will always be less than a Just and Just can only exist if we have a value to pass. Once we pass the lift_predicate we are going to get a Just or a Nothing depending on the data sent and the check.
Lift Custom Comparisons
Okay so now we want to deal with some custom comparisons. So we need to have them lifted in the Maybe logic.
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
%Nothing{}, %Just{} -> false
%Just{}, %Nothing{} -> false
end,
not_eq?: fn
%Just{value: v1}, %Just{value: v2} -> custom_eq.not_eq?.(v1, v2)
%Nothing{}, %Nothing{} -> false
%Nothing{}, %Just{} -> true
%Just{}, %Nothing{} -> true
end
}
end
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{}, %Nothing{} -> false
%Nothing{}, %Just{} -> true
%Just{}, %Nothing{} -> false
end,
le?: fn
%Just{value: v1}, %Just{value: v2} -> custom_ord.le?.(v1, v2)
%Nothing{}, %Nothing{} -> true
%Nothing{}, %Just{} -> true
%Just{}, %Nothing{} -> false
end,
gt?: fn
%Just{value: v1}, %Just{value: v2} -> custom_ord.gt?.(v1, v2)
%Nothing{}, %Nothing{} -> false
%Just{}, %Nothing{} -> true
%Nothing{}, %Just{} -> false
end,
ge?: fn
%Just{value: v1}, %Just{value: v2} -> custom_ord.ge?.(v1, v2)
%Nothing{}, %Nothing{} -> true
%Just{}, %Nothing{} -> true
%Nothing{}, %Just{} -> false
end
}
end
We are taking the idea of a custom Ord or an Eq and then making the rules for how they would work. In the case of the Eq we have the rule for the customeq running iff we have a _Just and a Just otherwise we would know the outcome.
Run It
iex(81)> alice=FunPark.Patron.make("Alice",15,150,ticket_tier: :vip)
%FunPark.Patron{
id: 910,
name: "Alice",
age: 15,
height: 150,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(82)> beth=FunPark.Patron.make("Beth",15,150)
%FunPark.Patron{
id: 974,
name: "Beth",
age: 15,
height: 150,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(83)> ord_by_ticket=FunPark.Patron.ord_by_ticket_tier()
%{
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>,
lt?: #Function<1.94864313/2 in FunPark.Ord.Utils.contramap/2>
}
iex(84)> FunPark.Ord.Utils.compare(alice,beth,ord_by_ticket)
:gt
iex(85)> alice_maybe=FunPark.Monad.Maybe.pure(alice)
%FunPark.Monad.Maybe.Just{
value: %FunPark.Patron{
id: 910,
name: "Alice",
age: 15,
height: 150,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
}
iex(86)> beth_maybe=FunPark.Monad.Maybe.pure(beth)
%FunPark.Monad.Maybe.Just{
value: %FunPark.Patron{
id: 974,
name: "Beth",
age: 15,
height: 150,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
}
iex(87)> lifted_ord=FunPark.Monad.Maybe.lift_ord(ord_by_ticket)
%{
ge?: #Function<12.37819108/2 in FunPark.Monad.Maybe.lift_ord/1>,
gt?: #Function<11.37819108/2 in FunPark.Monad.Maybe.lift_ord/1>,
le?: #Function<10.37819108/2 in FunPark.Monad.Maybe.lift_ord/1>,
lt?: #Function<9.37819108/2 in FunPark.Monad.Maybe.lift_ord/1>
}
iex(88)> FunPark.Ord.Utils.compare(alice_maybe,beth_maybe,lifted_ord)
:gt
Okay so we now can pass in a custom Ord or Eq. It works for both cases but now we can start to add more information to the functions.
Model Absence in a Monoid
• Nothing serves as the identity, explicitly representing the absence of a value.
• Just holds a result that combines according to the underlying monoid.
Let’s now revisit the Patron.max_priority_monoid\0.
# patron.ex
defp max_priority_monoid do
%Monoid.Max{
value: priority_empty(),
ord: ord_by_priority()
}
end
def max_priority_maybe_monoid do
%Monoid.Max{
value: Maybe.nothing(),
ord: Maybe.lift_ord(ord_by_priority())
}
end
def highest_priority_maybe(patrons) when is_list(patrons) do
m_concat(
max_priority_maybe_monoid(),
patrons |> Enum.map(&Maybe.pure/1)
)
end
This allows us to leverage the Maybe to get a real bottom wrung of the totem pole for non-existent value for order.
Run It
iex(90)> alice = FunPark.Patron.make("Alice", 15, 120, reward_points: 50, ticket_tier: :premium)
%FunPark.Patron{
id: 13,
name: "Alice",
age: 15,
height: 120,
ticket_tier: :premium,
fast_passes: [],
reward_points: 50,
likes: [],
dislikes: []
}
iex(91)> beth = FunPark.Patron.make("Beth", 16, 130, reward_points: 20, ticket_tier: :vip)
%FunPark.Patron{
id: 1038,
name: "Beth",
age: 16,
height: 130,
ticket_tier: :vip,
fast_passes: [],
reward_points: 20,
likes: [],
dislikes: []
}
iex(92)> charles = FunPark.Patron.make("Charles", 14, 135, reward_points: 150, ticket_tier: :premium)
%FunPark.Patron{
id: 1102,
name: "Charles",
age: 14,
height: 135,
ticket_tier: :premium,
fast_passes: [],
reward_points: 150,
likes: [],
dislikes: []
}
iex(93)> FunPark.Patron.highest_priority_maybe([alice, beth, charles])
%FunPark.Monad.Maybe.Just{
value: %FunPark.Patron{
id: 1038,
name: "Beth",
age: 16,
height: 130,
ticket_tier: :vip,
fast_passes: [],
reward_points: 20,
likes: [],
dislikes: []
}
}
iex(94)> FunPark.Patron.highest_priority_maybe([alice, charles])
%FunPark.Monad.Maybe.Just{
value: %FunPark.Patron{
id: 1102,
name: "Charles",
age: 14,
height: 135,
ticket_tier: :premium,
fast_passes: [],
reward_points: 150,
likes: [],
dislikes: []
}
}
iex(95)> FunPark.Patron.highest_priority_maybe([alice])
%FunPark.Monad.Maybe.Just{
value: %FunPark.Patron{
id: 13,
name: "Alice",
age: 15,
height: 120,
ticket_tier: :premium,
fast_passes: [],
reward_points: 50,
likes: [],
dislikes: []
}
}
iex(96)> FunPark.Patron.highest_priority_maybe([])
%FunPark.Monad.Maybe.Nothing{}
This built us a list of the Patrons in the order_by_priority as we set as the default priority.
Implement the Monadic Behaviors
We need to build the map/2, bind/2 or, ap/2 within the Maybe.
# nothing.ex
defimpl FunPark.Monad, for: FunPark.Monad.Maybe.Nothing do
alias FunPark.Monad.Maybe.Nothing
def map(%Nothing{}, _func), do: %Nothing{}
def ap(%Nothing{}, _val), do: %Nothing{}
def bind(%Nothing{}, _func), do: %Nothing{}
end
# just.ex
defimpl FunPark.Monad, for: FunPark.Monad.Maybe.Just do
alias FunPark.Monad.Maybe.{Just, Nothing}
def map(%Just{value: value}, func), do: Just.pure(func.(value))
def ap(%Just{value: func}, %Just{value: value}),
do: Just.pure(func.(value))
def ap(%Just{}, %Nothing{}), do: %Nothing{}
def bind(%Just{value: value}, func), do: func.(value)
end
• map/2 applies the transformation and wraps the result back in Just, preserv-
ing structure.
• ap/2 applies the wrapped function to the wrapped value if both are Just;
otherwise, it returns Nothing.
• bind/2 applies a function that returns a Maybe, transforming the value and
potentially changing structure.
Functor Map
We want to only update the ridetime if the _Ride is on-line. So we need to add in this function.
# ride.ex
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
Run It
iex(97)> tea_cup = FunPark.Ride.make("Tea Cup", online: false)
%FunPark.Ride{
id: 1166,
name: "Tea Cup",
min_age: 0,
min_height: 0,
wait_time: 0,
online: false,
tags: []
}
iex(98)> FunPark.Ride.update_wait_time_maybe(tea_cup, 20)
%FunPark.Monad.Maybe.Nothing{}
iex(99)> tea_cup = FunPark.Ride.change(tea_cup, %{online: true})
%FunPark.Ride{
id: 1166,
name: "Tea Cup",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
iex(100)> FunPark.Ride.update_wait_time_maybe(tea_cup, 20)
%FunPark.Monad.Maybe.Just{
value: %FunPark.Ride{
id: 1166,
name: "Tea Cup",
min_age: 0,
min_height: 0,
wait_time: 20,
online: true,
tags: []
}
}
Monad Bind
So now we can a way to forward a Patron based off of their FastPass eligibility for a Ride
def check_ride_eligibility(%Patron{} = patron, %__MODULE__{} = ride) do
is_eligible = curry_r(&eligible?/2)
Maybe.lift_predicate(patron, is_eligible.(ride))
end
def check_fast_pass(%Patron{} = patron, %__MODULE__{} = ride) do
has_fast_pass = curry_r(&fast_pass?/2)
Maybe.lift_predicate(patron, has_fast_pass.(ride))
end
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
We are building off the idea that we need to curry the data as we are comparing Ride and Patron here. We build the curry then pass that to the lift_predicate for both instances of the check. Once that is done you can bind to get an actual result.
Run It
iex(101)> haunted_mansion = FunPark.Ride.make("Haunted Mansion", min_age: 14)
%FunPark.Ride{
id: 1230,
name: "Haunted Mansion",
min_age: 14,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
iex(102)> datetime = DateTime.new!(~D[2025-06-01], ~T[13:00:00])
~U[2025-06-01 13:00:00Z]
iex(103)> fast_pass = FunPark.FastPass.make(haunted_mansion, datetime)
%FunPark.FastPass{
id: 1422,
ride: %FunPark.Ride{
id: 1230,
name: "Haunted Mansion",
min_age: 14,
min_height: 0,
wait_time: 0,
online: true,
tags: []
},
time: ~U[2025-06-01 13:00:00Z]
}
iex(104)> alice = FunPark.Patron.make("Alice", 15, 150)
%FunPark.Patron{
id: 1486,
name: "Alice",
age: 15,
height: 150,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(105)> beth = FunPark.Patron.make("Beth", 13, 130)
%FunPark.Patron{
id: 1550,
name: "Beth",
age: 13,
height: 130,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(106)> FunPark.Ride.fast_pass_lane(alice, haunted_mansion)
%FunPark.Monad.Maybe.Nothing{}
iex(107)> alice = FunPark.Patron.add_fast_pass(alice, fast_pass)
%FunPark.Patron{
id: 1486,
name: "Alice",
age: 15,
height: 150,
ticket_tier: :basic,
fast_passes: [
%FunPark.FastPass{
id: 1422,
ride: %FunPark.Ride{
id: 1230,
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(108)> FunPark.Ride.fast_pass_lane(alice, haunted_mansion)
%FunPark.Monad.Maybe.Just{
value: %FunPark.Patron{
id: 1486,
name: "Alice",
age: 15,
height: 150,
ticket_tier: :basic,
fast_passes: [
%FunPark.FastPass{
id: 1422,
ride: %FunPark.Ride{
id: 1230,
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(109)> beth = FunPark.Patron.add_fast_pass(beth, fast_pass)
%FunPark.Patron{
id: 1550,
name: "Beth",
age: 13,
height: 130,
ticket_tier: :basic,
fast_passes: [
%FunPark.FastPass{
id: 1422,
ride: %FunPark.Ride{
id: 1230,
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(110)> FunPark.Ride.fast_pass_lane(beth, haunted_mansion)
%FunPark.Monad.Maybe.Nothing{}
This allowed us to not only check for VIP and FastPass but also ride_eligibility
Recovery
Okay so we forgot that a VIP doesn’t need a FastPass so we need an orelse that we can pass a check with a fallback function then a new _check_vip_or_fast_pass
# maybe
def or_else(%Nothing{}, fallback_fun) when is_function(fallback_fun, 0),
do: fallback_fun.()
def or_else(%Just{} = just, _fallback_fun), do: just
# ride.ex
def check_vip_or_fast_pass(patron, ride) do
is_vip = &Patron.vip?/1
patron
|> Maybe.lift_predicate(is_vip)
|> Maybe.or_else(fn -> check_fast_pass(patron, ride) end)
end
def fast_pass_lane(%Patron{} = patron, %__MODULE__{} = ride) do
check_vip_or_pass = curry_r(&check_vip_or_fast_pass/2)
patron
|> check_ride_eligibility(ride)
|> bind(check_vip_or_pass.(ride))
end
Now we are using the new check_vip_or_fast_pass
Run It
iex(111)> charles = FunPark.Patron.make("Charles", 15, 145)
%FunPark.Patron{
id: 1614,
name: "Charles",
age: 15,
height: 145,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(112)> FunPark.Ride.fast_pass_lane(charles, haunted_mansion)
%FunPark.Monad.Maybe.Nothing{}
iex(113)> charles = FunPark.Patron.change(charles, %{ticket_tier: :vip})
%FunPark.Patron{
id: 1614,
name: "Charles",
age: 15,
height: 145,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(114)> FunPark.Ride.fast_pass_lane(charles, haunted_mansion)
%FunPark.Monad.Maybe.Just{
value: %FunPark.Patron{
id: 1614,
name: "Charles",
age: 15,
height: 145,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
}
Okay so we built a Patron checked availability, then added VIP and then checked again.
Refine Lists
Now we can move onto Lists.
Concat
Now we want to take all that and start to build lists. Let’s start with Maybe.concat
# maybe.ex
def concat(list) when is_list(list) do
list
|> fold_l([], fn
%Just{value: value}, acc -> [value | acc]
%Nothing{}, acc -> acc
end)
|> :lists.reverse()
end
# ride.ex
def only_fast_pass_lane(patrons, %__MODULE__{} = ride)
when is_list(patrons) do
patrons
|> Maybe.concat_map(&fast_pass_lane(&1, ride))
end
We now have the ability to build a map of Patrons take satisfy the conditions. Its basic Enum.reduce style construct.
Run It
iex(115)> haunted_mansion = FunPark.Ride.make("Haunted Mansion", min_age: 14)
%FunPark.Ride{
id: 1678,
name: "Haunted Mansion",
min_age: 14,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
iex(116)> datetime = DateTime.new!(~D[2025-06-01], ~T[13:00:00])
~U[2025-06-01 13:00:00Z]
iex(117)> fast_pass = FunPark.FastPass.make(haunted_mansion, datetime)
%FunPark.FastPass{
id: 1870,
ride: %FunPark.Ride{
id: 1678,
name: "Haunted Mansion",
min_age: 14,
min_height: 0,
wait_time: 0,
online: true,
tags: []
},
time: ~U[2025-06-01 13:00:00Z]
}
iex(118)> alice = FunPark.Patron.make("Alice", 15, 150)
%FunPark.Patron{
id: 1934,
name: "Alice",
age: 15,
height: 150,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(119)> beth = FunPark.Patron.make("Beth", 13, 135)
%FunPark.Patron{
id: 1998,
name: "Beth",
age: 13,
height: 135,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(120)> charles = FunPark.Patron.make("Charles", 15, 145)
%FunPark.Patron{
id: 2062,
name: "Charles",
age: 15,
height: 145,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(121)> alice = FunPark.Patron.add_fast_pass(alice, fast_pass)
%FunPark.Patron{
id: 1934,
name: "Alice",
age: 15,
height: 150,
ticket_tier: :basic,
fast_passes: [
%FunPark.FastPass{
id: 1870,
ride: %FunPark.Ride{
id: 1678,
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(122)> beth = FunPark.Patron.add_fast_pass(beth, fast_pass)
%FunPark.Patron{
id: 1998,
name: "Beth",
age: 13,
height: 135,
ticket_tier: :basic,
fast_passes: [
%FunPark.FastPass{
id: 1870,
ride: %FunPark.Ride{
id: 1678,
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(123)> charles = FunPark.Patron.change(charles, %{ticket_tier: :vip})
%FunPark.Patron{
id: 2062,
name: "Charles",
age: 15,
height: 145,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(124)> patrons = [alice, beth, charles]
[
%FunPark.Patron{
id: 1934,
name: "Alice",
age: 15,
height: 150,
ticket_tier: :basic,
fast_passes: [
%FunPark.FastPass{
id: 1870,
ride: %FunPark.Ride{
id: 1678,
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: []
},
%FunPark.Patron{
id: 1998,
name: "Beth",
age: 13,
height: 135,
ticket_tier: :basic,
fast_passes: [
%FunPark.FastPass{
id: 1870,
ride: %FunPark.Ride{
id: 1678,
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: []
},
%FunPark.Patron{
id: 2062,
name: "Charles",
age: 15,
height: 145,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
]
iex(125)> FunPark.Ride.only_fast_pass_lane_concat(patrons, haunted_mansion)
[
%FunPark.Patron{
id: 1934,
name: "Alice",
age: 15,
height: 150,
ticket_tier: :basic,
fast_passes: [
%FunPark.FastPass{
id: 1870,
ride: %FunPark.Ride{
id: 1678,
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: []
},
%FunPark.Patron{
id: 2062,
name: "Charles",
age: 15,
height: 145,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
]
This did exactly what we want but it also added in an extra Enum.map loop. We can do better.
Concat Map
# maybe.ex
def concat_map(list, func) when is_list(list) and is_function(func, 1) do
fold_l(list, [], fn item, acc ->
case func.(item) do
%Just{value: value} -> [value | acc]
%Nothing{} -> acc
end
end)
|> :lists.reverse()
end
# ride
def only_fast_pass_lane(patrons, %__MODULE__{} = ride)
when is_list(patrons) do
patrons
|> Maybe.concat_map(&fast_pass_lane(&1, ride))
end
This removes the extra Enum that will go through the values to check and puts it in the front.
Runt It
ex> patrons = [alice, beth, charles]
iex> FunPark.Ride.only_fast_pass_lane(patrons, haunted_mansion)
[
%FunPark.Patron{ name: "Alice", ... },
%FunPark.Patron{ name: "Charles", ... }
]
Sequence
Okay so now we want to work on returning a full list or nothing.
# maybe.ex
def sequence([]), do: pure([])
def sequence([head | tail]) do
bind(head, fn value ->
bind(sequence(tail), fn rest ->
pure([value | rest])
end)
end)
end
# ride.ex
def group_fast_pass_lane(patrons, %__MODULE__{} = ride)
when is_list(patrons) do
patrons
|> Enum.map(&fast_pass_lane(&1, ride))
|> Maybe.sequence()
end
This uses recursion to check for every Patron sent and then break-out if it fails.
Run It
iex(129)> patrons = [alice, beth, charles]
[
%FunPark.Patron{
id: 1934,
name: "Alice",
age: 15,
height: 150,
ticket_tier: :basic,
fast_passes: [
%FunPark.FastPass{
id: 1870,
ride: %FunPark.Ride{
id: 1678,
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: []
},
%FunPark.Patron{
id: 1998,
name: "Beth",
age: 13,
height: 135,
ticket_tier: :basic,
fast_passes: [
%FunPark.FastPass{
id: 1870,
ride: %FunPark.Ride{
id: 1678,
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: []
},
%FunPark.Patron{
id: 2062,
name: "Charles",
age: 15,
height: 145,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
]
iex(130)> FunPark.Ride.group_fast_pass_lane(patrons, haunted_mansion)
%FunPark.Monad.Maybe.Nothing{}
iex(131)> FunPark.Ride.group_fast_pass_lane([alice, charles], haunted_mansion)
%FunPark.Monad.Maybe.Just{
value: [
%FunPark.Patron{
id: 1934,
name: "Alice",
age: 15,
height: 150,
ticket_tier: :basic,
fast_passes: [
%FunPark.FastPass{
id: 1870,
ride: %FunPark.Ride{
id: 1678,
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: []
},
%FunPark.Patron{
id: 2062,
name: "Charles",
age: 15,
height: 145,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
]
}
Okay so now we can build based off the group that is asking.
Not so Fast
The current implementation. Using Enum will traverse the entire list instead of stopping prematurely if needed. We can build a better version ourselves.
Traverse
# maybe.ex
def traverse([], _func), do: pure([])
def traverse(list, func) when is_list(list) and is_function(func, 1) do
list
|> Enum.reduce_while(pure([]), fn item, %Just{value: acc} ->
case func.(item) do
%Just{value: value} -> {:cont, pure([value | acc])}
%Nothing{} -> {:halt, nothing()}
end
end)
|> map(&:lists.reverse/1)
end
# Ride
def group_fast_pass_lane(patrons, %__MODULE__{} = ride)
when is_list(patrons) do
Maybe.traverse(patrons, &fast_pass_lane(&1, ride))
end
Okay so now we have a way to break out early.
Filter Within Composition
Let’s start to think about filtering and Filterable.
defprotocol FunPark.Filterable do
def guard(structure, bool)
def filter(structure, predicate)
def filter_map(structure, func)
end
• guard/2 retains the value if the Boolean is true and discards it otherwise.
• filter/2 retains the value if the predicate passes.
• filter_map/2 applies a transformation that may also discard the value.
# nothing.ex
defimpl FunPark.Filterable, for: FunPark.Monad.Maybe.Nothing do
alias FunPark.Monad.Maybe.Nothing
def guard(%Nothing{}, _boolean), do: %Nothing{}
def filter(%Nothing{}, _predicate), do: %Nothing{}
def filter_map(%Nothing{}, _func), do: %Nothing{}
end
# just.ex
defimpl FunPark.Filterable, for: FunPark.Monad.Maybe.Just do
alias FunPark.Monad.Maybe
alias FunPark.Monad.Maybe.Just
alias FunPark.Monad
def guard(%Just{} = maybe, true), do: maybe
def guard(%Just{}, false), do: Maybe.nothing()
def filter(%Just{} = maybe, predicate) do
Monad.bind(maybe, fn value ->
if predicate.(value) do
Maybe.pure(value)
else
Maybe.nothing()
end
end)
end
def filter_map(%Just{value: value}, func) do
case func.(value) do
%Just{} = just -> just
_ -> Maybe.nothing()
end
end
end
• guard/2 retains the original Just if the condition is true; otherwise, it returns
Nothing.
• filter/2 applies a predicate to the contained value. If the predicate returns
true, it keeps the value in a Just; if not, it returns Nothing.
• filter_map/2 applies a transformation that returns a Maybe. If the result is a
Just, it’s returned; otherwise, it returns Nothing.
Guard
Okay so this is a way to make sure that we filter out bad values. In this case wait-times that are less than or equal to 0.
def update_wait_time_maybe(%__MODULE__{} = ride, wait_time)
when is_number(wait_time) do
ride
|> Maybe.lift_predicate(&online?/1)
|> guard(wait_time >= 0)
|> map(&update_wait_time(&1, wait_time))
end
Now we have a way to deal with bad wait times. We check for if it’s online then check if it has a valid wait-time.
Run It
iex(132)> tea_cup = FunPark.Ride.make("Tea Cup")
%FunPark.Ride{
id: 2126,
name: "Tea Cup",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
iex(133)> FunPark.Ride.update_wait_time_maybe(tea_cup, 10)
%FunPark.Monad.Maybe.Just{
value: %FunPark.Ride{
id: 2126,
name: "Tea Cup",
min_age: 0,
min_height: 0,
wait_time: 10,
online: true,
tags: []
}
}
iex(134)> FunPark.Ride.update_wait_time_maybe(tea_cup, -10)
%FunPark.Monad.Maybe.Nothing{}
iex(135)> tea_cup = FunPark.Ride.change(tea_cup, %{online: false})
%FunPark.Ride{
id: 2126,
name: "Tea Cup",
min_age: 0,
min_height: 0,
wait_time: 0,
online: false,
tags: []
}
iex(136)> FunPark.Ride.update_wait_time_maybe(tea_cup, 10)
%FunPark.Monad.Maybe.Nothing{}
This is that same logic in action.
Filter
def add_fast_pass_maybe(%__MODULE__{} = patron, fast_pass) do
ride = FastPass.get_ride(fast_pass)
new_passes = List.union([fast_pass], get_fast_passes(patron))
update_fast_pass = Utils.curry_r(&change/2)
eligible = Utils.curry_r(&Ride.eligible?/2)
patron
|> Maybe.pure()
|> filter(eligible.(ride))
|> map(update_fast_pass.(%{fast_passes: new_passes}))
end
FilterMap
Now we want to bind/2 and filter_map/2
def fast_pass_lane(%Patron{} = patron, %__MODULE__{} = ride) do
check_vip_or_pass = curry_r(&check_vip_or_fast_pass/2)
patron
|> check_ride_eligibility(ride)
|> bind(check_vip_or_pass.(ride))
end
# 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)
# |> filter_map(check_vip_or_pass)
# end
What You’ve Learned
So with this chapter at a close we need to think about all that we have learned. The Maybe is a set of structs that need to be impl at every lever for each type of struct that will be passed. We will need to deal with the Nothing and the Just as these are different types. Once we have built those we can then start to work with the Identity that we can build off of. We then case start to deal with some defaults that will take values as structs and then give us a Just or a Nothing or even a fall-back.
Once this is all set we can then start to deal with the other Context that we have built Eq and Ord to start. Those will need to be defined within the Just and Nothing but once they are defined you can build off of them.
We then started to work with the Maybe version of all the functions that we have built so far. It requires more from you to start but in the end you will deal with more types of functions and you will have better defaults and ways of getting out of loops faster.
There is a lot to talk about here and the Act on it will help with some version of this.