We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
9. Model Outcomes with Either
So now we can talk about some sort of validation. When you try to buy a plane ticket and you get these errors when you try to submit the purchase.
• Name field cannot be blank.
• Return date must be after departure date.
• Phone number is invalid.
While Maybe captures the possibility of a value. Either represents the possibility of success.
Structure of Either
First we need to use branching logic of Right and Left, one for success and one for failure.
defmodule FunPark.Monad.Either.Right do
@enforce_keys [:right]
defstruct [:right]
def pure(value), do: %__MODULE__{right: value}
end
defmodule FunPark.Monad.Either.Left do
@enforce_keys [:left]
defstruct [:left]
def pure(value), do: %__MODULE__{left: value}
end
This is maybe but says more.
Eq
A Right is equal if their inner values are equal, same with Lefts. A Right is never equal to a Left even if the values are the same.
Ord
Same idea here with but in this case a Left is always less than a Right.
Foldable
Either can be folded into a single result.
Filterable
Either can’t generate a new Left when a predicate fails.
Monad
The Either context supports monadic composition through map/2, bind/2, and ap/2. These work exactly as they do in Maybe, with Left taking the place of Nothing.
• map/2 transforms the Right value, leaving Left untouched.
• bind/2 sequences computations that may fail, propagating the first Left encountered.
• ap/2 applies a Right(function) to a Right(value); if either is a Left, the failure short-circuits.
Either Module
This module sets up the functions for constructing, inspecting, and adapting Either values. This will mirror Maybe but is adapted for cases where both branches carry data.
We can build/construct using right/1, left/1, or the more general pure/1. Then we can check which branch we’re working with using right?/1 and left/1.
We will also be able to use lift: lift_maybe/2, lift_predicate/3, lift_eq/1, lift_ord/1. We can also interoperate with native Elixir using from_result/1, to_result/1, from from_try/1 and to_try!/1.
Validation
Now we heard from out Ride expert, they say that people are upset that they don’t know why they are not getting a fast pass.
Ride Eligibility
We will build Right(eligible_patron) and Left(reason_for_denial).
def lift_predicate(value, predicate, on_false)
when is_function(predicate, 1) and is_function(on_false, 1) do
fold_l(
fn -> predicate.(value) end,
fn -> right(value) end,
fn -> left(on_false.(value)) end
)
end
This will allow us to have a thunked callback to a generate the Left value.
We want to now add in a service (an organization tool, grouping related behavior under a shared namespace) for the FastLane. The will ensurethat the height requirements are within our new FastLane service:
defmodule FunPark.Ride.FastLane do
def ensure_height(%Patron{} = patron, %Ride{} = ride) do
patron
|> Either.lift_predicate(
curry_r(&Ride.tall_enough?/2).(ride),
fn p -> "#{Patron.get_name(p)} is not tall enough" end
)
|> Either.map_left(&ValidationError.new/1)
end
end
So this will take the lift_predicate/3 as well as the patron and the check and then allow us to build both the left and the right.
Now let’s build the ensure_age/2 as well.
def ensure_age(%Patron{} = patron, %Ride{} = ride) do
patron
|> Either.lift_predicate(
curry_r(&Ride.old_enough?/2).(ride),
fn p -> "#{Patron.get_name(p)} is not old enough" end
)
|> Either.map_left(&ValidationError.new/1)
end
Both of these use the map_left\2 which will return the Right if no error or the Left if there is an error
def map_left(%Left{left: error}, func), do: left(func.(error))
def map_left(%Right{} = right, _), do: right
Okay so let’s take a second to go over the ValidationError as that is a big part of the handling of the errors.
defmodule FunPark.Errors.ValidationError do
defstruct errors: [], __exception__: true
@behaviour Exception
def new(errors) when is_list(errors), do: %__MODULE__{errors: errors}
def new(error), do: %__MODULE__{errors: [error]}
def merge(%__MODULE__{errors: e1}, %__MODULE__{errors: e2}),
do: %__MODULE__{errors: e1 ++ e2}
@impl Exception
def exception(args) when is_list(args), do: struct(__MODULE__, args)
@impl Exception
def exception(message) when is_binary(message),
do: %__MODULE__{errors: [message]}
@impl Exception
def message(%__MODULE__{errors: errors}) do
Enum.map_join(errors, ", ", &to_string/1)
end
end
This is how we will deal with errors. There is 2 things here I will say then Ill give a more in-depth explanation of the functions. First ValidationError will hold a list of errors messages. Second there is no built-in validation error type in Elixir, so that is where the @impl comes in.
• new/1: Wraps a single message or a list of messages.
• merge/2: Combines two structs by joining their message lists.
• exception/1: Builds the struct from a keyword list or a single string.
• message/1: Returns a formatted string by joining the error messages.
There is a redefining of the Exception behaviour within this module.
Run it
iex(1)> alice = FunPark.Patron.make("Alice", 12, 125, ticket_tier: :vip)
%FunPark.Patron{
id: 6274,
name: "Alice",
age: 12,
height: 125,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(2)>
nil
iex(3)> beth = FunPark.Patron.make("Beth", 16, 115)
%FunPark.Patron{
id: 23,
name: "Beth",
age: 16,
height: 115,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(4)>
nil
iex(5)> haunted_mansion =
...(5)> FunPark.Ride.make(
...(5)> "Haunted Mansion",
...(5)> min_age: 14,
...(5)> min_height: 120
...(5)> )
%FunPark.Ride{
id: 6338,
name: "Haunted Mansion",
min_age: 14,
min_height: 120,
wait_time: 0,
online: true,
tags: []
}
iex(6)>
nil
iex(7)> FunPark.Ride.FastLane.ensure_height(alice, haunted_mansion)
%FunPark.Monad.Either.Right{
right: %FunPark.Patron{
id: 6274,
name: "Alice",
age: 12,
height: 125,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
}
iex(8)>
nil
iex(9)> FunPark.Ride.FastLane.ensure_age(alice, haunted_mansion)
%FunPark.Monad.Either.Left{
left: %FunPark.Errors.ValidationError{errors: ["Alice is not old enough"]}
}
iex(10)>
nil
iex(11)> FunPark.Ride.FastLane.ensure_age(beth, haunted_mansion)
%FunPark.Monad.Either.Right{
right: %FunPark.Patron{
id: 23,
name: "Beth",
age: 16,
height: 115,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
}
iex(12)>
nil
iex(13)> FunPark.Ride.FastLane.ensure_height(beth, haunted_mansion)
%FunPark.Monad.Either.Left{
left: %FunPark.Errors.ValidationError{errors: ["Beth is not tall enough"]}
}
As you can see we are able to use the FastLand that we made to give good feedback if the check fails.
Combining Eligibility
# fast_lane.ex
def ensure_eligibility(%Patron{} = patron, %Ride{} = ride) do
validate_height = curry_r(&ensure_height/2)
patron
|> ensure_age(ride)
|> bind(validate_height.(ride))
end
We take both of the new functions and curry them into one check
iex(14)> FunPark.Ride.FastLane.ensure_eligibility(alice, haunted_mansion)
%FunPark.Monad.Either.Left{
left: %FunPark.Errors.ValidationError{errors: ["Alice is not old enough"]}
}
iex(15)>
nil
iex(16)> FunPark.Ride.FastLane.ensure_eligibility(beth, haunted_mansion)
%FunPark.Monad.Either.Left{
left: %FunPark.Errors.ValidationError{errors: ["Beth is not tall enough"]}
}
iex(17)>
nil
iex(18)> charles = FunPark.Patron.make("Charles", 13, 115)
%FunPark.Patron{
id: 6402,
name: "Charles",
age: 13,
height: 115,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(19)>
nil
iex(20)> FunPark.Ride.FastLane.ensure_eligibility(charles, haunted_mansion)
%FunPark.Monad.Either.Left{
left: %FunPark.Errors.ValidationError{errors: ["Charles is not old enough"]}
}
iex(21)> dave = FunPark.Patron.make( "Dave", 16, 140)
%FunPark.Patron{
id: 6466,
name: "Dave",
age: 16,
height: 140,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(22)> FunPark.Ride.FastLane.ensure_eligibility(dave, haunted_mansion)
%FunPark.Monad.Either.Right{
right: %FunPark.Patron{
id: 6466,
name: "Dave",
age: 16,
height: 140,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
}
Using the new function we can check for eligibility.
Ensure a FastPass
def ensure_fast_pass(%Patron{} = patron, %Ride{} = ride) do
patron
|> Either.lift_predicate(
curry_r(&Ride.fast_pass?/2).(ride),
fn p -> "#{Patron.get_name(p)} does not have a fast pass" end
)
|> Either.map_left(&ValidationError.new/1)
end
Run it
iex(31)> FunPark.Ride.FastLane.ensure_fast_pass(dave, haunted_mansion)
%FunPark.Monad.Either.Right{
right: %FunPark.Patron{
id: 6466,
name: "Dave",
age: 16,
height: 140,
ticket_tier: :basic,
fast_passes: [
%FunPark.FastPass{
id: 87,
ride: %FunPark.Ride{
id: 6338,
name: "Haunted Mansion",
min_age: 14,
min_height: 120,
wait_time: 0,
online: true,
tags: []
},
time: ~U[2025-06-01 13:00:00Z]
}
],
reward_points: 0,
likes: [],
dislikes: []
}
}
iex(32)>
nil
iex(33)> elsie = FunPark.Patron.make("Elsie", 17, 135, ticket_tier: :vip)
%FunPark.Patron{
id: 151,
name: "Elsie",
age: 17,
height: 135,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(34)>
nil
iex(35)> FunPark.Ride.FastLane.ensure_fast_pass(elsie, haunted_mansion)
%FunPark.Monad.Either.Left{
left: %FunPark.Errors.ValidationError{
errors: ["Elsie does not have a fast pass"]
}
}
Using the new logic we are able to give feedback on whether the Patron has a fast pass.
Ensure a FastPass or VIP Access
def ensure_vip_or_fast_pass(%Patron{} = patron, %Ride{} = ride) do
patron
|> Either.lift_predicate(
&Patron.vip?/1,
fn p -> "#{Patron.get_name(p)} is not a VIP" end
)
|> Either.map_left(&ValidationError.new/1)
|> Either.or_else(fn -> ensure_fast_pass(patron, ride) end)
end
iex(36)> FunPark.Ride.FastLane.ensure_vip_or_fast_pass(elsie, haunted_mansion)
%FunPark.Monad.Either.Right{
right: %FunPark.Patron{
id: 151,
name: "Elsie",
age: 17,
height: 135,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
}
Ensure Fast Lane Access
def ensure_fast_pass_lane(%Patron{} = patron, %Ride{} = ride) do
ensure_vip_or_pass = curry_r(&ensure_vip_or_fast_pass/2)
patron
|> ensure_eligibility(ride)
|> bind(ensure_vip_or_pass.(ride))
end
Run it
iex(37)> FunPark.Ride.FastLane.ensure_fast_pass_lane(alice, haunted_mansion)
%FunPark.Monad.Either.Left{
left: %FunPark.Errors.ValidationError{errors: ["Alice is not old enough"]}
}
iex(38)> FunPark.Ride.FastLane.ensure_fast_pass_lane(beth, haunted_mansion)
%FunPark.Monad.Either.Left{
left: %FunPark.Errors.ValidationError{errors: ["Beth is not tall enough"]}
}
iex(39)> FunPark.Ride.FastLane.ensure_fast_pass_lane(charles, haunted_mansion)
%FunPark.Monad.Either.Left{
left: %FunPark.Errors.ValidationError{errors: ["Charles is not old enough"]}
}
iex(40)> FunPark.Ride.FastLane.ensure_fast_pass_lane(dave, haunted_mansion)
%FunPark.Monad.Either.Right{
right: %FunPark.Patron{
id: 6466,
name: "Dave",
age: 16,
height: 140,
ticket_tier: :basic,
fast_passes: [
%FunPark.FastPass{
id: 87,
ride: %FunPark.Ride{
id: 6338,
name: "Haunted Mansion",
min_age: 14,
min_height: 120,
wait_time: 0,
online: true,
tags: []
},
time: ~U[2025-06-01 13:00:00Z]
}
],
reward_points: 0,
likes: [],
dislikes: []
}
}
iex(41)> FunPark.Ride.FastLane.ensure_fast_pass_lane(elsie, haunted_mansion)
%FunPark.Monad.Either.Right{
right: %FunPark.Patron{
id: 151,
name: "Elsie",
age: 17,
height: 135,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
}
Okay so now that we have all of this we can check for each person for each ride and then get feedback if they don’t meet any of the requirements. But now we can start to check against groups.
Ensure Groups of Patrons
def ensure_fast_pass_lane_group(
patrons,
%Ride{} = ride
)
when is_list(patrons) do
eligible_for_fast_lane = curry_r(&ensure_fast_pass_lane/2)
Either.traverse(
patrons,
eligible_for_fast_lane.(ride)
)
end
iex(42)> patrons = [elsie, dave]
[
%FunPark.Patron{
id: 151,
name: "Elsie",
age: 17,
height: 135,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
},
%FunPark.Patron{
id: 6466,
name: "Dave",
age: 16,
height: 140,
ticket_tier: :basic,
fast_passes: [
%FunPark.FastPass{
id: 87,
ride: %FunPark.Ride{
id: 6338,
name: "Haunted Mansion",
min_age: 14,
min_height: 120,
wait_time: 0,
online: true,
tags: []
},
time: ~U[2025-06-01 13:00:00Z]
}
],
reward_points: 0,
likes: [],
dislikes: []
}
]
iex(43)> FunPark.Ride.FastLane.ensure_fast_pass_lane_group(patrons, haunted_mansion)
%FunPark.Monad.Either.Right{
right: [
%FunPark.Patron{
id: 151,
name: "Elsie",
age: 17,
height: 135,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
},
%FunPark.Patron{
id: 6466,
name: "Dave",
age: 16,
height: 140,
ticket_tier: :basic,
fast_passes: [
%FunPark.FastPass{
id: 87,
ride: %FunPark.Ride{
id: 6338,
name: "Haunted Mansion",
min_age: 14,
min_height: 120,
wait_time: 0,
online: true,
tags: []
},
time: ~U[2025-06-01 13:00:00Z]
}
],
reward_points: 0,
likes: [],
dislikes: []
}
]
}
iex(44)> patrons = [elsie, dave, charles]
[
%FunPark.Patron{
id: 151,
name: "Elsie",
age: 17,
height: 135,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
},
%FunPark.Patron{
id: 6466,
name: "Dave",
age: 16,
height: 140,
ticket_tier: :basic,
fast_passes: [
%FunPark.FastPass{
id: 87,
ride: %FunPark.Ride{
id: 6338,
name: "Haunted Mansion",
min_age: 14,
min_height: 120,
wait_time: 0,
online: true,
tags: []
},
time: ~U[2025-06-01 13:00:00Z]
}
],
reward_points: 0,
likes: [],
dislikes: []
},
%FunPark.Patron{
id: 6402,
name: "Charles",
age: 13,
height: 115,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
]
iex(45)> FunPark.Ride.FastLane.ensure_fast_pass_lane_group(patrons, haunted_mansion)
%FunPark.Monad.Either.Left{
left: %FunPark.Errors.ValidationError{errors: ["Charles is not old enough"]}
}
iex(46)> patrons = [elsie, dave, charles, beth]
[
%FunPark.Patron{
id: 151,
name: "Elsie",
age: 17,
height: 135,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
},
%FunPark.Patron{
id: 6466,
name: "Dave",
age: 16,
height: 140,
ticket_tier: :basic,
fast_passes: [
%FunPark.FastPass{
id: 87,
ride: %FunPark.Ride{
id: 6338,
name: "Haunted Mansion",
min_age: 14,
min_height: 120,
wait_time: 0,
online: true,
tags: []
},
time: ~U[2025-06-01 13:00:00Z]
}
],
reward_points: 0,
likes: [],
dislikes: []
},
%FunPark.Patron{
id: 6402,
name: "Charles",
age: 13,
height: 115,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
},
%FunPark.Patron{
id: 23,
name: "Beth",
age: 16,
height: 115,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
]
iex(47)> FunPark.Ride.FastLane.ensure_fast_pass_lane_group(patrons, haunted_mansion)
%FunPark.Monad.Either.Left{
left: %FunPark.Errors.ValidationError{errors: ["Charles is not old enough"]}
}
iex(48)> patrons = [elsie, dave, beth, charles]
[
%FunPark.Patron{
id: 151,
name: "Elsie",
age: 17,
height: 135,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
},
%FunPark.Patron{
id: 6466,
name: "Dave",
age: 16,
height: 140,
ticket_tier: :basic,
fast_passes: [
%FunPark.FastPass{
id: 87,
ride: %FunPark.Ride{
id: 6338,
name: "Haunted Mansion",
min_age: 14,
min_height: 120,
wait_time: 0,
online: true,
tags: []
},
time: ~U[2025-06-01 13:00:00Z]
}
],
reward_points: 0,
likes: [],
dislikes: []
},
%FunPark.Patron{
id: 23,
name: "Beth",
age: 16,
height: 115,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
},
%FunPark.Patron{
id: 6402,
name: "Charles",
age: 13,
height: 115,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
]
iex(49)> FunPark.Ride.FastLane.ensure_fast_pass_lane_group(patrons, haunted_mansion)
%FunPark.Monad.Either.Left{
left: %FunPark.Errors.ValidationError{errors: ["Beth is not tall enough"]}
}
Okay so we are using the final Monad to test against a group of Patrons
From Bind to Combine
We have a new concern. We need a way to combine all the errors so that we see every error, if Beth can’t go and Alice can’t go Beth won’t be able to see the issues until they get to the front.
def traverse_a([], _func), do: right([])
def traverse_a(list, func) when is_list(list) and is_function(func, 1) do
fold_l(list, right([]), fn item, acc_result ->
case {func.(item), acc_result} do
{%Right{right: value}, %Right{right: acc}} ->
right([value | acc])
{%Left{left: new}, %Left{left: existing}} ->
left(append(existing, coerce(new)))
{%Right{}, %Left{left: existing}} ->
left(existing)
{%Left{left: err}, %Right{}} ->
left(coerce(err))
end
end)
|> map(&:lists.reverse/1)
end
If all items pass then we get a Right if anyone of them fail we get a Left. With this we need to append all the results into one list so we need to implement an Appendable.
defprotocol FunPark.Appendable do
@fallback_to_any true
def coerce(term)
def append(accumulator, coerced)
end
defimpl FunPark.Appendable, for: Any do
def coerce(value) when is_list(value), do: value
def coerce(value), do: [value]
def append(acc, value), do: coerce(acc) ++ coerce(value)
end
# validation_error.ex
defimpl FunPark.Appendable, for: FunPark.Errors.ValidationError do
alias FunPark.Errors.ValidationError
def coerce(%ValidationError{errors: e}), do: ValidationError.new(e)
def append(%ValidationError{} = acc, %ValidationError{} = value) do
ValidationError.merge(acc, value)
end
end
This will allow us to take any number of errors and append them to the list as we get them.
• coerce/1 calls new/1, ensuring the errors field is always a list.
• append/2 combines two ValidationError structs, using merge/2.
Run It
iex(50)> alice = FunPark.Patron.make("Alice", 12, 125, ticket_tier: :vip)
%FunPark.Patron{
id: 215,
name: "Alice",
age: 12,
height: 125,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(51)> beth = FunPark.Patron.make("Beth", 16, 115)
%FunPark.Patron{
id: 279,
name: "Beth",
age: 16,
height: 115,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(52)> charles = FunPark.Patron.make("Charles", 13, 115)
%FunPark.Patron{
id: 6658,
name: "Charles",
age: 13,
height: 115,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(53)> dave = FunPark.Patron.make("Dave", 16, 140)
%FunPark.Patron{
id: 6722,
name: "Dave",
age: 16,
height: 140,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(54)> elsie = FunPark.Patron.make("Elsie", 17, 135, ticket_tier: :vip)
%FunPark.Patron{
id: 6786,
name: "Elsie",
age: 17,
height: 135,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(55)> haunted_mansion = FunPark.Ride.make("Haunted Mansion", min_age: 14, min_height: 120)
%FunPark.Ride{
id: 6850,
name: "Haunted Mansion",
min_age: 14,
min_height: 120,
wait_time: 0,
online: true,
tags: []
}
iex(56)> valid_height = FunPark.Utils.curry_r(&FunPark.Ride.FastLane.ensure_height/2)
#Function<3.64560675/1 in FunPark.Utils.curry_r/3>
iex(57)> valid_age = FunPark.Utils.curry_r(&FunPark.Ride.FastLane.ensure_age/2)
#Function<3.64560675/1 in FunPark.Utils.curry_r/3>
iex(58)> validators = [valid_height.(haunted_mansion), valid_age.(haunted_mansion)]
[#Function<2.64560675/1 in FunPark.Utils.curry_r/3>,
#Function<2.64560675/1 in FunPark.Utils.curry_r/3>]
iex(59)> FunPark.Monad.Either.traverse_a(validators, & &1.(alice))
%FunPark.Monad.Either.Left{
left: %FunPark.Errors.ValidationError{errors: ["Alice is not old enough"]}
}
iex(60)> FunPark.Monad.Either.traverse_a(validators, & &1.(beth))
%FunPark.Monad.Either.Left{
left: %FunPark.Errors.ValidationError{errors: ["Beth is not tall enough"]}
}
iex(61)> FunPark.Monad.Either.traverse_a(validators, & &1.(charles))
%FunPark.Monad.Either.Left{
left: %FunPark.Errors.ValidationError{
errors: ["Charles is not tall enough", "Charles is not old enough"]
}
}
iex(62)> FunPark.Monad.Either.traverse_a(validators, & &1.(dave))
%FunPark.Monad.Either.Right{
right: [
%FunPark.Patron{
id: 6722,
name: "Dave",
age: 16,
height: 140,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
},
%FunPark.Patron{
id: 6722,
name: "Dave",
age: 16,
height: 140,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
]
}
iex(63)> FunPark.Monad.Either.validate(dave, validators)
%FunPark.Monad.Either.Right{
right: %FunPark.Patron{
id: 6722,
name: "Dave",
age: 16,
height: 140,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
}
iex(64)> FunPark.Monad.Either.validate(charles, validators)
%FunPark.Monad.Either.Left{
left: %FunPark.Errors.ValidationError{
errors: ["Charles is not tall enough", "Charles is not old enough"]
}
}
We now build the Patrons, Rides, and FastPass then we check against the different validators. Once we traverse_a we can build a list of errors or the full list of rides that pass the tests. Again if any of them fail we get a Left. This is good but we get a Right lists of Daves at the end. We want to just show a single Dave if we pass.
# either.ex
def validate(value, validators) when is_list(validators) do
traverse_a(validators, fn validator -> validator.(value) end)
|> map(fn _ -> value end)
end
Now let’s run that.
iex(65)> FunPark.Monad.Either.validate(dave, validators)
%FunPark.Monad.Either.Right{
right: %FunPark.Patron{
id: 6722,
name: "Dave",
age: 16,
height: 140,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
}
iex(66)> FunPark.Monad.Either.validate(charles, validators)
%FunPark.Monad.Either.Left{
left: %FunPark.Errors.ValidationError{
errors: ["Charles is not tall enough", "Charles is not old enough"]
}
}
Okay so now we can just get back the list of the errors, or the Patron. We can add more to the FastLane and then add in the functionality to the Ride
# fast_lane.ex
def validate_eligibility(%Patron{} = patron, %Ride{} = ride) do
validate_height = curry_r(&ensure_height/2)
validate_age = curry_r(&ensure_age/2)
patron
|> Either.validate([validate_height.(ride), validate_age.(ride)])
end
def validate_fast_pass_lane(%Patron{} = patron, %Ride{} = ride) do
validate_eligibility = curry(&validate_eligibility/2)
validate_vip_or_pass = curry(&ensure_vip_or_fast_pass/2)
Either.validate(
ride,
[
validate_eligibility.(patron),
validate_vip_or_pass.(patron),
&Ride.ensure_online/1
]
)
|> map(fn _ -> patron end)
end
# ride.ex
def ensure_online(%__MODULE__{} = ride) do
Either.lift_predicate(
ride,
&online?/1,
fn r -> "#{r.name} is offline" end
)
|> Either.map_left(&ValidationError.new/1)
end
Run It
iex(67)> FunPark.Ride.FastLane.validate_fast_pass_lane(alice, haunted_mansion)
%FunPark.Monad.Either.Left{
left: %FunPark.Errors.ValidationError{errors: ["Alice is not old enough"]}
}
iex(68)> FunPark.Ride.FastLane.validate_fast_pass_lane(beth, haunted_mansion)
%FunPark.Monad.Either.Left{
left: %FunPark.Errors.ValidationError{
errors: ["Beth is not tall enough", "Beth does not have a fast pass"]
}
}
iex(69)> FunPark.Ride.FastLane.validate_fast_pass_lane(charles, haunted_mansion)
%FunPark.Monad.Either.Left{
left: %FunPark.Errors.ValidationError{
errors: ["Charles is not tall enough", "Charles is not old enough",
"Charles does not have a fast pass"]
}
}
iex(70)> FunPark.Ride.FastLane.validate_fast_pass_lane(dave, haunted_mansion)
%FunPark.Monad.Either.Left{
left: %FunPark.Errors.ValidationError{
errors: ["Dave does not have a fast pass"]
}
}
iex(71)> datetime = DateTime.new!(~D[2025-06-01], ~T[13:00:00])
~U[2025-06-01 13:00:00Z]
iex(72)> fast_pass = FunPark.FastPass.make(haunted_mansion, datetime)
%FunPark.FastPass{
id: 7106,
ride: %FunPark.Ride{
id: 6850,
name: "Haunted Mansion",
min_age: 14,
min_height: 120,
wait_time: 0,
online: true,
tags: []
},
time: ~U[2025-06-01 13:00:00Z]
}
iex(73)> dave = FunPark.Patron.add_fast_pass(dave, fast_pass)
%FunPark.Patron{
id: 6722,
name: "Dave",
age: 16,
height: 140,
ticket_tier: :basic,
fast_passes: [
%FunPark.FastPass{
id: 7106,
ride: %FunPark.Ride{
id: 6850,
name: "Haunted Mansion",
min_age: 14,
min_height: 120,
wait_time: 0,
online: true,
tags: []
},
time: ~U[2025-06-01 13:00:00Z]
}
],
reward_points: 0,
likes: [],
dislikes: []
}
iex(74)> FunPark.Ride.FastLane.validate_fast_pass_lane(dave, haunted_mansion)
%FunPark.Monad.Either.Right{
right: %FunPark.Patron{
id: 6722,
name: "Dave",
age: 16,
height: 140,
ticket_tier: :basic,
fast_passes: [
%FunPark.FastPass{
id: 7106,
ride: %FunPark.Ride{
id: 6850,
name: "Haunted Mansion",
min_age: 14,
min_height: 120,
wait_time: 0,
online: true,
tags: []
},
time: ~U[2025-06-01 13:00:00Z]
}
],
reward_points: 0,
likes: [],
dislikes: []
}
}
iex(75)> FunPark.Ride.FastLane.validate_fast_pass_lane(elsie, haunted_mansion)
%FunPark.Monad.Either.Right{
right: %FunPark.Patron{
id: 6786,
name: "Elsie",
age: 17,
height: 135,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
}
iex(76)> haunted_mansion = FunPark.Ride.change(haunted_mansion, %{online: false})
%FunPark.Ride{
id: 6850,
name: "Haunted Mansion",
min_age: 14,
min_height: 120,
wait_time: 0,
online: false,
tags: []
}
iex(77)> FunPark.Ride.FastLane.validate_fast_pass_lane(elsie, haunted_mansion)
%FunPark.Monad.Either.Left{
left: %FunPark.Errors.ValidationError{errors: ["Haunted Mansion is offline"]}
}
We built a new error handler for the Ride and then added all the tests to the validate_fast_pass_lane/2 so that we can then check against all the tests.
Don’t Look, Just Fly
Okay so now we want to test against an entire group and then pass or fail the entire group. You should give reasons if they fail.
def validate_fast_pass_lane_group(
patrons,
%Ride{} = ride
)
when is_list(patrons) do
validate_fast_lane = curry_r(&validate_fast_pass_lane/2)
patrons
|> Either.traverse_a(validate_fast_lane.(ride))
end
Run It
iex(78)> haunted_mansion = FunPark.Ride.change(haunted_mansion, %{online: true})
%FunPark.Ride{
id: 6850,
name: "Haunted Mansion",
min_age: 14,
min_height: 120,
wait_time: 0,
online: true,
tags: []
}
iex(79)> patrons = [alice, beth, charles, dave, elsie]
[
%FunPark.Patron{
id: 215,
name: "Alice",
age: 12,
height: 125,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
},
%FunPark.Patron{
id: 279,
name: "Beth",
age: 16,
height: 115,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
},
%FunPark.Patron{
id: 6658,
name: "Charles",
age: 13,
height: 115,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
},
%FunPark.Patron{
id: 6722,
name: "Dave",
age: 16,
height: 140,
ticket_tier: :basic,
fast_passes: [
%FunPark.FastPass{
id: 7106,
ride: %FunPark.Ride{
id: 6850,
name: "Haunted Mansion",
min_age: 14,
min_height: 120,
wait_time: 0,
online: true,
tags: []
},
time: ~U[2025-06-01 13:00:00Z]
}
],
reward_points: 0,
likes: [],
dislikes: []
},
%FunPark.Patron{
id: 6786,
name: "Elsie",
age: 17,
height: 135,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
]
iex(80)> FunPark.Ride.FastLane.validate_fast_pass_lane_group(patrons, haunted_mansion)
%FunPark.Monad.Either.Left{
left: %FunPark.Errors.ValidationError{
errors: ["Alice is not old enough", "Beth is not tall enough",
"Beth does not have a fast pass", "Charles is not tall enough",
"Charles is not old enough", "Charles does not have a fast pass"]
}
}
iex(81)> patrons = [dave, elsie]
[
%FunPark.Patron{
id: 6722,
name: "Dave",
age: 16,
height: 140,
ticket_tier: :basic,
fast_passes: [
%FunPark.FastPass{
id: 7106,
ride: %FunPark.Ride{
id: 6850,
name: "Haunted Mansion",
min_age: 14,
min_height: 120,
wait_time: 0,
online: true,
tags: []
},
time: ~U[2025-06-01 13:00:00Z]
}
],
reward_points: 0,
likes: [],
dislikes: []
},
%FunPark.Patron{
id: 6786,
name: "Elsie",
age: 17,
height: 135,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
]
iex(82)> FunPark.Ride.FastLane.validate_fast_pass_lane_group(patrons, haunted_mansion)
%FunPark.Monad.Either.Right{
right: [
%FunPark.Patron{
id: 6722,
name: "Dave",
age: 16,
height: 140,
ticket_tier: :basic,
fast_passes: [
%FunPark.FastPass{
id: 7106,
ride: %FunPark.Ride{
id: 6850,
name: "Haunted Mansion",
min_age: 14,
min_height: 120,
wait_time: 0,
online: true,
tags: []
},
time: ~U[2025-06-01 13:00:00Z]
}
],
reward_points: 0,
likes: [],
dislikes: []
},
%FunPark.Patron{
id: 6786,
name: "Elsie",
age: 17,
height: 135,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
]
}
Now that we have a list of in-eligible Patrons and the function for it we now get a list of reasons why the group fails.
Make Errors Explicit
The runtime can and should be able to use the errors to get a better view of the system and why it failed.
A Store for FunPark
Let’s use ETS to create a table for the Errors
Create Table
defmodule FunPark.Store do
import FunPark.Monad
alias FunPark.Monad.Either
def create_table(table) when is_atom(table) do
Either.from_try(fn ->
:ets.new(table, [:named_table, :set, :public])
end)
end
def drop_table(table) when is_atom(table) do
Either.from_try(fn ->
:ets.delete(table)
end)
|> map(fn _ -> table end)
end
def insert_item(table, %{id: id} = item) when is_atom(table) do
Either.from_try(fn ->
:ets.insert(table, {id, Map.from_struct(item)})
end)
|> map(fn _ -> item end)
end
def get_item(table, id) when is_atom(table) do
Either.from_try(fn ->
:ets.lookup(table, id)
end)
|> bind(fn
[{_id, item}] -> Either.pure(item)
[] -> Either.left(:not_found)
end)
end
def get_all_items(table) when is_atom(table) do
Either.from_try(fn ->
:ets.tab2list(table)
end)
|> map(fn items ->
Enum.map(items, fn {_, item} -> item end)
end)
end
def delete_item(table, id) when is_atom(table) do
Either.from_try(fn ->
:ets.delete(table, id)
end)
|> map(fn _ -> id end)
end
end
Ride Repository
defmodule FunPark.Ride.Repo do
import FunPark.Monad
import FunPark.Utils, only: [curry: 1]
alias FunPark.Monad.Either
alias FunPark.List
alias FunPark.Ride
alias FunPark.Store
@table_name :rides
def create_table do
Store.create_table(@table_name)
end
def save(%Ride{} = ride) do
insert_ride = curry(&Store.insert_item/2)
ride
|> Ride.validate()
|> bind(insert_ride.(@table_name))
end
def get(id) when is_integer(id) do
Store.get_item(@table_name, id)
|> map(fn data -> struct(Ride, data) end)
|> Either.map_left(fn _ -> :not_found end)
end
def list() do
Store.get_all_items(@table_name)
|> map(fn items ->
items
|> Enum.map(fn item -> struct(Ride, item) end)
|> List.sort()
end)
|> Either.get_or_else([])
end
def delete(%Ride{id: id}) do
Store.delete_item(@table_name, id)
|> Either.get_or_else(:ok)
end
end
# ride.ex (we need to validate a ride first)
def validate(%__MODULE__{} = ride) do
Either.validate(
ride,
[
&ensure_min_age/1,
&ensure_min_height/1,
&ensure_wait_time/1,
&ensure_name/1
]
)
end
Run It
iex(83)> FunPark.Ride.Repo.create_table()
%FunPark.Monad.Either.Right{right: :rides}
iex(84)> banana = FunPark.Ride.make("Banana Slip")
%FunPark.Ride{
id: 535,
name: "Banana Slip",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
iex(85)> apple = FunPark.Ride.make("Apple Cart")
%FunPark.Ride{
id: 599,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
iex(86)> FunPark.Ride.Repo.save(banana)
%FunPark.Monad.Either.Right{
right: %FunPark.Ride{
id: 535,
name: "Banana Slip",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
}
iex(87)> FunPark.Ride.Repo.save(apple)
%FunPark.Monad.Either.Right{
right: %FunPark.Ride{
id: 599,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
}
iex(88)> bad_apple = FunPark.Ride.change(apple, %{wait_time: -1, min_age: -1})
%FunPark.Ride{
id: 599,
name: "Apple Cart",
min_age: -1,
min_height: 0,
wait_time: -1,
online: true,
tags: []
}
iex(89)> FunPark.Ride.Repo.save(bad_apple)
%FunPark.Monad.Either.Left{
left: %FunPark.Errors.ValidationError{
errors: ["Apple Cart: min age must be non negative",
"Apple Cart: wait time must be non negative"]
}
}
iex(90)> FunPark.Ride.Repo.get(banana.id)
%FunPark.Monad.Either.Right{
right: %FunPark.Ride{
id: 535,
name: "Banana Slip",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
}
iex(91)> FunPark.Ride.Repo.get(apple.id)
%FunPark.Monad.Either.Right{
right: %FunPark.Ride{
id: 599,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
}
iex(92)> FunPark.Ride.Repo.list()
[
%FunPark.Ride{
id: 599,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
},
%FunPark.Ride{
id: 535,
name: "Banana Slip",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
]
iex(93)> FunPark.Ride.Repo.delete(apple)
599
iex(94)> FunPark.Ride.Repo.get(apple.id)
%FunPark.Monad.Either.Left{left: :not_found}
iex(95)> FunPark.Ride.Repo.delete(apple)
599
iex(96)> FunPark.Ride.Repo.delete(apple)
599
iex(97)> FunPark.Store.drop_table(:rides)
%FunPark.Monad.Either.Right{right: :rides}
iex(98)> FunPark.Ride.Repo.delete(apple)
:ok
iex(99)> FunPark.Ride.Repo.get(apple.id)
%FunPark.Monad.Either.Left{left: :not_found}
iex(100)> FunPark.Ride.Repo.list()
[]
We now have a table that will keep track of the rides and will even validate them before we add them to the list. We then can delete them from the table.
What You’ve Learned
This is a great way to deal with errors and to then populate the lists or errors that we find. Keep this in mind when you are building an error condition, and feedback. It might feel like a lot of work but you will get a lot of functionality from the work you put in.