We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
10. Coordinate Tasks with Effect
We will now build a boundary for external effects with Task. This is sometimes called I/O (input/output). This new module will use Task but will work around the issues that Task can have. It is eager, it will raise errors instead of keeping them in context, and will break the boundary between definition and execution.
Build the Effect
We will us the Left and Right here for our branching logic. They will be Effect.Left and Effect.Right.
defmodule FunPark.Monad.Effect.Right do
alias FunPark.Monad.Either
defstruct [:effect]
def pure(value) do
%__MODULE__{
effect: fn _env ->
Task.async(fn -> Either.pure(value) end)
end
}
end
end
defmodule FunPark.Monad.Effect.Left do
alias FunPark.Monad.Either
defstruct [:effect]
def pure(value) do
%__MODULE__{
effect: fn _env ->
Task.async(fn -> Either.left(value) end)
end
}
end
end
We are setting up the environment for the Task here. One will build an Either the other will build the Left version of the Either.
# lib/fun_park/monad/effect.ex
def run(%_{effect: thunk}, env \\ %{}), do: execute_effect(thunk.(env))
defp execute_effect(task) do
start_time = System.monotonic_time(:millisecond)
result =
try do
case Task.yield(task, 5000) || Task.shutdown(task) do
{:ok, %Either.Right{} = right} ->
right
{:ok, %Either.Left{} = left} ->
left
{:ok, other} ->
Either.left(
EffectError.new(
:run,
{:invalid_result, other}
)
)
nil ->
Either.left(EffectError.new(:run, :timeout))
end
rescue
error -> Either.left(EffectError.new(:run, error))
end
elapsed = System.monotonic_time(:millisecond) - start_time
IO.puts("Task completed in #{elapsed}ms")
result
end
This sets up the way in which we will accomplish the task that we want. It will:
When we run an Effect, several outcomes are possible:
• {:ok, %Either.Right{}}: The task completed and returned a success.
• {:ok, %Either.Left{}}: The task completed and returned a failure.
• {:ok, other}: The task completed, but the result wasn’t an Either.
• nil: The task didn’t respond within the timeout.
• Exception: The task process unexpectedly crashed during execution.
The first two represent expected I/O results. The rest are unexpected, so we
wrap them in an EffectError, signaling invalid outputs, timeouts, or crashes.
run/2 is the protected execution boundary for our Effect, preventing unbounded
concurrency, isolating crashes, enforcing timeouts, rejecting invalid outputs,
and ensuring all failures are captured.
Run It
iex(1)> effect = FunPark.Monad.Effect.pure(40)
%FunPark.Monad.Effect.Right{
effect: #Function<2.34611373/1 in FunPark.Monad.Effect.Right.pure/1>
}
iex(2)> FunPark.Monad.Effect.run(effect)
Task completed in 3ms
%FunPark.Monad.Either.Right{right: 40}
iex(3)> bomb = fn -> raise "boom" end
#Function<43.39164016/0 in :erl_eval.expr/6>
iex(4)> bomb_effect = FunPark.Monad.Effect.lift_func(bomb)
%FunPark.Monad.Effect.Right{
effect: #Function<4.9888490/1 in FunPark.Monad.Effect.lift_func/1>
}
iex(5)> FunPark.Monad.Effect.run(bomb_effect)
Task completed in 2ms
%FunPark.Monad.Either.Left{
left: %FunPark.Errors.EffectError{
stage: :lift_func,
reason: %RuntimeError{message: "boom"}
}
}
iex(6)> long_delay = fn -> Process.sleep(6000) end
#Function<43.39164016/0 in :erl_eval.expr/6>
iex(7)> long_delay_effect = FunPark.Monad.Effect.lift_func(long_delay)
%FunPark.Monad.Effect.Right{
effect: #Function<4.9888490/1 in FunPark.Monad.Effect.lift_func/1>
}
iex(8)> FunPark.Monad.Effect.run(long_delay_effect)
Task completed in 5051ms
%FunPark.Monad.Either.Left{
left: %FunPark.Errors.EffectError{stage: :run, reason: :timeout}
}
We are taking the effect we want and then passing them to the Effect module to run them through the Task.
Implement Protocols
This is a deferred computation, so it doesn’t include Eq, Ord, or Foldable but the values can be compared after they are run.
Effect Module
def lift_func(thunk) when is_function(thunk, 0) do
%Right{
effect: fn _env ->
Task.async(fn ->
try do
Either.pure(thunk.())
rescue
error -> Either.left(EffectError.new(:lift_func, error))
end
end)
end
}
end
def lift_predicate(value, predicate, on_false) do
if predicate.(value), do: right(value), else: left(on_false.(value))
end
def lift_either(thunk) when is_function(thunk, 0) do
%Right{
effect: fn _env ->
Task.async(fn ->
try do
case thunk.() do
%Either.Right{} = right -> right
%Either.Left{} = left -> left
end
rescue
error ->
Either.left(EffectError.new(:lift_either, error))
end
end)
end
}
end
def lift_maybe(%Just{value: value}, _on_none), do: right(value)
def lift_maybe(%Nothing{}, on_none), do: left(on_none.())
# def map_left(%Right{} = r, _func), do: r
def map_left(%Right{effect: eff}, func) do
%Right{
effect: fn env ->
Task.async(fn ->
case Task.await(eff.(env)) do
%Either.Right{right: r} -> Either.pure(r)
%Either.Left{left: l} -> Either.left(func.(l))
end
end)
end
}
end
def from_result({:ok, v}), do: right(v)
def from_result({:error, e}), do: left(e)
def to_result(effect) do
case run(effect) do
%Either.Right{right: v} -> {:ok, v}
%Either.Left{left: e} -> {:error, e}
end
end
def from_try(func) when is_function(func, 1) do
fn value ->
%Right{
effect: fn _env ->
Task.async(fn ->
Either.from_try(fn -> func.(value) end)
end)
end
}
end
end
def to_try!(effect) do
effect
|> run()
|> Either.to_try!()
end
def sequence(list), do: traverse(list, & &1)
def traverse([], _), do: pure([])
def traverse([h | t], f) do
case f.(h) do
%Left{} = l ->
l
%Right{effect: e1} ->
case traverse(t, f) do
%Left{} = l ->
l
%Right{effect: e2} ->
%Right{
effect: fn env ->
Task.async(fn ->
with %Either.Right{right: x} <- run(%Right{effect: e1}, env),
%Either.Right{right: xs} <- run(%Right{effect: e2}, env) do
%Either.Right{right: [x | xs]}
else
%Either.Left{} = left -> left
end
end)
end
}
end
end
end
def sequence_a(list), do: traverse_a(list, & &1)
def traverse_a([], _func), do: right([])
def traverse_a(list, func) when is_list(list) and is_function(func, 1) do
%Right{
effect: fn env ->
Task.async(fn ->
tasks =
Enum.map(list, fn item ->
func.(item)
|> spawn_effect(env)
end)
results = Enum.map(tasks, &collect_result/1)
{oks, errs} =
Enum.split_with(results, fn
{:ok, _} -> true
{:error, _} -> false
end)
case errs do
[] ->
values = Enum.map(oks, fn {:ok, val} -> val end)
%Either.Right{right: values}
_ ->
errors =
errs
|> Enum.map(fn {:error, val} -> coerce(val) end)
|> Enum.reduce(&append(&1, &2))
%Either.Left{left: errors}
end
end)
end
}
end
We will need to build the above to work with the new Module. These are all needed to deal with all the tasks that we will need.
Deferred Transformation
defimpl FunPark.Monad, for: FunPark.Monad.Effect.Left do
alias FunPark.Monad.Effect.Left
def map(%Left{} = left, _transform), do: left
def bind(%Left{} = left, _func), do: left
def ap(%Left{} = left, _func), do: left
end
This implies that we are having and Effect failed state and will need to just output the error right away.
defimpl FunPark.Monad, for: FunPark.Monad.Effect.Right do
alias FunPark.Errors.EffectError
alias FunPark.Monad.{Effect, Either}
alias Effect.{Left, Right}
def map(%Right{effect: effect}, transform) do
%Right{
effect: fn env ->
Task.async(fn ->
case Effect.run(%Right{effect: effect}, env) do
%Either.Right{right: value} ->
try do
Either.pure(transform.(value))
rescue
e -> Either.left(EffectError.new(:map, e))
end
%Either.Left{} = left ->
left
end
end)
end
}
end
def bind(%Right{effect: effect}, kleisli_fn) do
%Right{
effect: fn env ->
Task.async(fn ->
case Effect.run(%Right{effect: effect}, env) do
%Either.Right{right: value} ->
try do
kleisli_fn.(value) |> Effect.run(env)
rescue
e -> Either.left(EffectError.new(:bind, e))
end
%Either.Left{} = left ->
left
end
end)
end
}
end
def ap(%Right{effect: effect_func}, %Right{effect: effect_value}) do
%Right{
effect: fn env ->
Task.async(fn ->
with %Either.Right{right: func} <- Effect.run(%Right{effect: effect_func}, env),
%Either.Right{right: value} <- Effect.run(%Right{effect: effect_value}, env) do
try do
Either.pure(right: func.(value))
rescue
e -> Either.left(EffectError.new(:ap, e))
end
else
%Either.Left{} = left -> left
end
end)
end
}
end
def ap(%Right{}, %Left{} = left), do: left
end
We are just using the map/2 at the moment but these will allow us to map a value and have many escapes to a Left if anything fails.
Run It
iex(9)> five_effect = FunPark.Monad.Effect.pure(5)
%FunPark.Monad.Effect.Right{
effect: #Function<2.34611373/1 in FunPark.Monad.Effect.Right.pure/1>
}
iex(10)> increment = fn v -> v + 1 end
#Function<42.39164016/1 in :erl_eval.expr/6>
iex(11)> six_effect = five_effect |> FunPark.Monad.map(increment)
%FunPark.Monad.Effect.Right{
effect: #Function<2.796317/1 in FunPark.Monad.FunPark.Monad.Effect.Right.map/2>
}
iex(12)> FunPark.Monad.Effect.run(six_effect)
Task completed in 0ms
Task completed in 0ms
%FunPark.Monad.Either.Right{right: 6}
iex(13)> alpha_effect = FunPark.Monad.Effect.pure("A")
%FunPark.Monad.Effect.Right{
effect: #Function<2.34611373/1 in FunPark.Monad.Effect.Right.pure/1>
}
iex(14)> error_effect = alpha_effect |> FunPark.Monad.map(increment)
%FunPark.Monad.Effect.Right{
effect: #Function<2.796317/1 in FunPark.Monad.FunPark.Monad.Effect.Right.map/2>
}
iex(15)> FunPark.Monad.Effect.run(error_effect)
Task completed in 0ms
Task completed in 2ms
%FunPark.Monad.Either.Left{
left: %FunPark.Errors.EffectError{
stage: :map,
reason: %ArithmeticError{message: "bad argument in arithmetic expression"}
}
}
iex(16)> increment_effect = FunPark.Monad.Effect.from_try(increment)
#Function<2.9888490/1 in FunPark.Monad.Effect.from_try/1>
iex(17)> error_effect_bind = alpha_effect |> FunPark.Monad.bind(increment_effect)
%FunPark.Monad.Effect.Right{
effect: #Function<1.796317/1 in FunPark.Monad.FunPark.Monad.Effect.Right.bind/2>
}
iex(18)> FunPark.Monad.Effect.run(error_effect_bind)
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
%FunPark.Monad.Either.Left{
left: %ArithmeticError{message: "bad argument in arithmetic expression"}
}
iex(19)> delay = fn value -> Process.sleep(500); value end
#Function<42.39164016/1 in :erl_eval.expr/6>
iex(20)> long_six_effect = six_effect |> FunPark.Monad.map(delay)
%FunPark.Monad.Effect.Right{
effect: #Function<2.796317/1 in FunPark.Monad.FunPark.Monad.Effect.Right.map/2>
}
iex(21)> FunPark.Monad.Effect.run(long_six_effect)
Task completed in 1ms
Task completed in 1ms
Task completed in 545ms
%FunPark.Monad.Either.Right{right: 6}
This took the standard error handling that is built-in to Elixir and used the try/rescue to propagate the errors or values through the system.
Effectful Store
Okay so now we heard again from the Ride expert and we need to be able to store more than just a boolean for the online status of a ride. We then need to build separate tables for: scheduled maintenance, unscheduled maintenance, compliance hold, fault-triggered lockout.
For now we want to add in a few functions that will allow us to add/2, get/2, and remove/2 from a table.
defmodule FunPark.Maintenance.Store do
import FunPark.Monad
alias FunPark.Monad.Effect
alias FunPark.Ride
alias FunPark.Store
def create_table(table) do
Store.create_table(table)
end
def add(%Ride{} = ride, table) do
Effect.lift_either(fn -> Store.insert_item(table, ride) end)
|> map(&simulate_delay/1)
|> Effect.map_left(&simulate_delay/1)
end
def remove(%Ride{id: id}, table) do
Effect.lift_either(fn -> Store.delete_item(table, id) end)
|> map(&simulate_delay/1)
|> Effect.map_left(&simulate_delay/1)
end
def get(%Ride{id: id}, table) do
Effect.lift_either(fn -> Store.get_item(table, id) end)
|> map(&simulate_delay/1)
|> Effect.map_left(&simulate_delay/1)
end
defp simulate_delay(ride) do
Process.sleep(500)
ride
end
end
Run It
iex(22)> FunPark.Store.create_table(:schedule)
%FunPark.Monad.Either.Right{right: :schedule}
iex(23)> apple = FunPark.Ride.make("Apple Cart")
%FunPark.Ride{
id: 3586,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
iex(24)> save_effect = FunPark.Maintenance.Store.add(apple, :schedule)
%FunPark.Monad.Effect.Right{
effect: #Function<5.9888490/1 in FunPark.Monad.Effect.map_left/2>
}
iex(25)> get_effect = FunPark.Maintenance.Store.get(apple, :schedule)
%FunPark.Monad.Effect.Right{
effect: #Function<5.9888490/1 in FunPark.Monad.Effect.map_left/2>
}
iex(26)> remove_effect = FunPark.Maintenance.Store.remove(apple, :schedule)
%FunPark.Monad.Effect.Right{
effect: #Function<5.9888490/1 in FunPark.Monad.Effect.map_left/2>
}
iex(27)> FunPark.Monad.Effect.run(save_effect)
Task completed in 1ms
Task completed in 508ms
%FunPark.Monad.Either.Right{
right: %FunPark.Ride{
id: 3586,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
}
iex(28)> FunPark.Monad.Effect.run(get_effect)
Task completed in 0ms
Task completed in 502ms
%FunPark.Monad.Either.Right{
right: %{
id: 3586,
name: "Apple Cart",
wait_time: 0,
min_age: 0,
min_height: 0,
tags: [],
online: true
}
}
iex(29)> FunPark.Monad.Effect.run(remove_effect)
Task completed in 0ms
Task completed in 507ms
%FunPark.Monad.Either.Right{right: 3586}
iex(30)> FunPark.Monad.Effect.run(get_effect)
Task completed in 0ms
Task completed in 503ms
%FunPark.Monad.Either.Left{left: :not_found}
iex(31)> FunPark.Store.drop_table(:schedule)
%FunPark.Monad.Either.Right{right: :schedule}
iex(32)> FunPark.Monad.Effect.run(get_effect)
Task completed in 6ms
Task completed in 515ms
%FunPark.Monad.Either.Left{
left: %ArgumentError{
message: "errors were found at the given arguments:\n\n * 1st argument: the table identifier does not refer to an existing ETS table\n"
}
}
We are adding Effects and then running them with the Effect. We have created a table that we can call with the atom that we name it by. Then we are using all the functions that we just created.
Maintenance Repository
Now we want to create all the tables for the different maintenance services.
Generate Tables
defmodule FunPark.Maintenance.Repo do
import FunPark.Monad, only: [bind: 2, map: 2]
alias FunPark.Monad.Effect
alias FunPark.Monad.Either
alias FunPark.Maintenance.Store
alias FunPark.Ride
def create_store do
Either.sequence_a([
Store.create_table(:schedule),
Store.create_table(:unschedule),
Store.create_table(:lockout),
Store.create_table(:compliance)
])
end
end
What is great about all this logic is that even if the table creation has an error we still ran it through the Either so we will get a Left if it fails.
Run It
iex(33)> FunPark.Maintenance.Repo.create_store()
%FunPark.Monad.Either.Right{
right: [:schedule, :unschedule, :lockout, :compliance]
}
iex(34)> FunPark.Maintenance.Repo.create_store()
%FunPark.Monad.Either.Left{
left: [
%ArgumentError{
message: "errors were found at the given arguments:\n\n * 1st argument: table name already exists\n"
},
%ArgumentError{
message: "errors were found at the given arguments:\n\n * 1st argument: table name already exists\n"
},
%ArgumentError{
message: "errors were found at the given arguments:\n\n * 1st argument: table name already exists\n"
},
%ArgumentError{
message: "errors were found at the given arguments:\n\n * 1st argument: table name already exists\n"
}
]
}
You can see that if we already have the table we will get a Left of the errors.
Save a Ride
We need to validate the ride and then bind the ride once validated to the table that we are going to add it to.
defp validate_ride_effect(ride) do
Effect.lift_either(fn -> Ride.validate(ride) end)
end
defp add_to_store_effect(valid_ride) do
Effect.asks(fn env -> env[:table] end)
|> bind(fn table -> Store.add(valid_ride, table) end)
end
def add_ride_effect(%Ride{} = ride) do
validate_ride_effect(ride)
|> bind(&add_to_store_effect/1)
end
Run It
iex(35)> FunPark.Maintenance.Repo.create_store()
iex(36)> apple = FunPark.Ride.make("Apple Cart")
%FunPark.Ride{
id: 3650,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
iex(37)> effect = FunPark.Maintenance.Repo.add_ride_effect(apple)
%FunPark.Monad.Effect.Right{
effect: #Function<1.796317/1 in FunPark.Monad.FunPark.Monad.Effect.Right.bind/2>
}
iex(38)> FunPark.Monad.Effect.run(effect, %{table: :schedule})
Task completed in 8ms
Task completed in 0ms
Task completed in 0ms
Task completed in 506ms
Task completed in 506ms
Task completed in 514ms
%FunPark.Monad.Either.Right{
right: %FunPark.Ride{
id: 3650,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
}
There is no functionality yet to automatically set the correct table but once we have the right and the Effect with a ride and a table to send it to we are all set. We can now set a specific event for adding a Ride to the schedule table.
def add_schedule(%Ride{} = ride) do
ride
|> add_ride_effect()
|> Effect.run(%{table: :schedule})
end
def add_unschedule(%Ride{} = ride) do
ride
|> add_ride_effect()
|> Effect.run(%{table: :unschedule})
end
def add_lockout(%Ride{} = ride) do
ride
|> add_ride_effect()
|> Effect.run(%{table: :lockout})
end
def add_compliance(%Ride{} = ride) do
ride
|> add_ride_effect()
|> Effect.run(%{table: :compliance})
end
I put all the functions here but there will be a add_to_all/1 we build later.
Run It
iex(39)> invalid_apple = FunPark.Ride.change(apple, %{wait_time: -10})
%FunPark.Ride{
id: 3650,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: -10,
online: true,
tags: []
}
iex(40)> FunPark.Maintenance.Repo.add_schedule(invalid_apple)
Task completed in 5ms
Task completed in 6ms
%FunPark.Monad.Either.Left{
left: %FunPark.Errors.ValidationError{
errors: ["Apple Cart: wait time must be non negative"]
}
}
iex(41)> FunPark.Store.drop_table(:schedule)
%FunPark.Monad.Either.Right{right: :schedule}
iex(42)> FunPark.Maintenance.Repo.add_schedule(apple)
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 516ms
Task completed in 516ms
Task completed in 516ms
%FunPark.Monad.Either.Left{
left: %ArgumentError{
message: "errors were found at the given arguments:\n\n * 1st argument: the table identifier does not refer to an existing ETS table\n"
}
}
You can see the errors being handled as we send things through the events.
Save to All
def add_to_all(%Ride{} = ride) do
ride
|> Repo.add_schedule()
|> bind(&Repo.add_unschedule/1)
|> bind(&Repo.add_lockout/1)
|> bind(&Repo.add_compliance/1)
end
This will run in the Either context and as such it will run sequentially. So if we want parallelism we are painted into a corner.
Run It
iex(43)> apple = FunPark.Ride.make("Apple Cart")
%FunPark.Ride{
id: 3714,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
iex(44)> FunPark.Maintenance.Repo.create_store()
%FunPark.Monad.Either.Left{
left: [
%ArgumentError{
message: "errors were found at the given arguments:\n\n * 1st argument: table name already exists\n"
},
%ArgumentError{
message: "errors were found at the given arguments:\n\n * 1st argument: table name already exists\n"
},
%ArgumentError{
message: "errors were found at the given arguments:\n\n * 1st argument: table name already exists\n"
}
]
}
iex(45)> FunPark.Maintenance.add_to_all(apple)
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 515ms
Task completed in 515ms
Task completed in 515ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 551ms
Task completed in 551ms
Task completed in 551ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 522ms
Task completed in 522ms
Task completed in 522ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 513ms
Task completed in 513ms
Task completed in 513ms
%FunPark.Monad.Either.Right{
right: %FunPark.Ride{
id: 3714,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
}
This is it validating, then adding in the _Effect_ then adding it to the table.
Remove from All
Let’s add in the ability to remove from a table.
def remove_schedule(%Ride{} = ride) do
ride
|> remove_ride_effect()
|> Effect.run(%{table: :schedule})
end
def remove_unschedule(%Ride{} = ride) do
ride
|> remove_ride_effect()
|> Effect.run(%{table: :unschedule})
end
def remove_lockout(%Ride{} = ride) do
ride
|> remove_ride_effect()
|> Effect.run(%{table: :lockout})
end
def remove_compliance(%Ride{} = ride) do
ride
|> remove_ride_effect()
|> Effect.run(%{table: :compliance})
end
def remove_from_all(%Ride{} = ride) do
Either.sequence_a([
Repo.remove_schedule(ride),
Repo.remove_unschedule(ride),
Repo.remove_lockout(ride),
Repo.remove_compliance(ride)
])
|> map(fn _ -> ride end)
end
def remove_ride_effect(%Ride{} = ride) do
validate_ride_effect(ride)
|> bind(&remove_from_store_effect/1)
|> map(fn _ -> ride end)
end
We will need the Effect that we will pass to the Task but once we build that we have the ability to remove a Ride from any and all tables.
Run It
iex(46)> FunPark.Maintenance.Repo.create_store()
%FunPark.Monad.Either.Left{
left: [
%ArgumentError{
message: "errors were found at the given arguments:\n\n * 1st argument: table name already exists\n"
},
%ArgumentError{
message: "errors were found at the given arguments:\n\n * 1st argument: table name already exists\n"
},
%ArgumentError{
message: "errors were found at the given arguments:\n\n * 1st argument: table name already exists\n"
},
%ArgumentError{
message: "errors were found at the given arguments:\n\n * 1st argument: table name already exists\n"
}
]
}
iex(47)> apple = FunPark.Ride.make("Apple Cart")
%FunPark.Ride{
id: 3778,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
iex(48)> FunPark.Maintenance.add_to_all(apple)
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 501ms
Task completed in 501ms
Task completed in 501ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 511ms
Task completed in 511ms
Task completed in 511ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 503ms
Task completed in 503ms
Task completed in 503ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 501ms
Task completed in 501ms
Task completed in 501ms
%FunPark.Monad.Either.Right{
right: %FunPark.Ride{
id: 3778,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
}
iex(49)> FunPark.Maintenance.remove_from_all(apple)
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 511ms
Task completed in 511ms
Task completed in 511ms
Task completed in 511ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 501ms
Task completed in 501ms
Task completed in 501ms
Task completed in 501ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 506ms
Task completed in 506ms
Task completed in 506ms
Task completed in 506ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 513ms
Task completed in 513ms
Task completed in 513ms
Task completed in 513ms
%FunPark.Monad.Either.Right{
right: %FunPark.Ride{
id: 3778,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
}
iex(50)> invalid_apple = FunPark.Ride.change(apple, %{wait_time: -10})
%FunPark.Ride{
id: 3778,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: -10,
online: true,
tags: []
}
iex(51)> FunPark.Maintenance.add_to_all(invalid_apple)
Task completed in 0ms
Task completed in 0ms
%FunPark.Monad.Either.Left{
left: %FunPark.Errors.ValidationError{
errors: ["Apple Cart: wait time must be non negative"]
}
}
iex(52)> FunPark.Maintenance.remove_from_all(invalid_apple)
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
%FunPark.Monad.Either.Left{
left: %FunPark.Errors.ValidationError{
errors: ["Apple Cart: wait time must be non negative",
"Apple Cart: wait time must be non negative",
"Apple Cart: wait time must be non negative",
"Apple Cart: wait time must be non negative"]
}
}
iex(53)> FunPark.Store.drop_table(:schedule)
%FunPark.Monad.Either.Right{right: :schedule}
iex(54)> FunPark.Maintenance.add_to_all(apple)
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 512ms
Task completed in 513ms
Task completed in 513ms
%FunPark.Monad.Either.Left{
left: %ArgumentError{
message: "errors were found at the given arguments:\n\n * 1st argument: the table identifier does not refer to an existing ETS table\n"
}
}
iex(55)> FunPark.Maintenance.remove_from_all(apple)
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 551ms
Task completed in 551ms
Task completed in 551ms
Task completed in 551ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 501ms
Task completed in 501ms
Task completed in 501ms
Task completed in 501ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 507ms
Task completed in 507ms
Task completed in 507ms
Task completed in 507ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 501ms
Task completed in 501ms
Task completed in 501ms
Task completed in 501ms
%FunPark.Monad.Either.Left{
left: [
%ArgumentError{
message: "errors were found at the given arguments:\n\n * 1st argument: the table identifier does not refer to an existing ETS table\n"
}
]
}
This is the raw input and output for the Tasks some of the response are a bit out of sync, but you can see that at anoint you are allowed to add and remove valid Rides and you will get errors if you try an invalid Ride.
Inject Behavior, Not Configuration
Okay so now that we are here we can talk about injecting the Behaviour not the Config. Right now we have a spefici implmenation of the store and we want to be able to send the behaviour and then let it work itself out.
def has_ride_effect(%Ride{} = ride, table) do
Effect.asks(fn env -> env[:store] end)
|> bind(fn store -> store.get(ride, table) end)
|> map(fn _ -> ride end)
end
def in_schedule(%Ride{} = ride), do: has_ride_effect(ride, :schedule)
def in_unschedule(%Ride{} = ride), do: has_ride_effect(ride, :unschedule)
def in_lockout(%Ride{} = ride), do: has_ride_effect(ride, :lockout)
def in_compliance(%Ride{} = ride), do: has_ride_effect(ride, :compliance)
Run It
iex(56)> FunPark.Maintenance.Repo.create_store()
%FunPark.Monad.Either.Left{
left: [
%ArgumentError{
message: "errors were found at the given arguments:\n\n * 1st argument: table name already exists\n"
},
%ArgumentError{
message: "errors were found at the given arguments:\n\n * 1st argument: table name already exists\n"
},
%ArgumentError{
message: "errors were found at the given arguments:\n\n * 1st argument: table name already exists\n"
}
]
}
iex(57)> apple = FunPark.Ride.make("Apple Cart")
%FunPark.Ride{
id: 3842,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
iex(58)> effect = FunPark.Maintenance.Repo.in_schedule(apple)
%FunPark.Monad.Effect.Right{
effect: #Function<2.796317/1 in FunPark.Monad.FunPark.Monad.Effect.Right.map/2>
}
iex(59)> env = %{store: FunPark.Maintenance.Store}
%{store: FunPark.Maintenance.Store}
iex(60)> FunPark.Monad.Effect.run(effect, env)
Task completed in 0ms
Task completed in 0ms
Task completed in 504ms
Task completed in 506ms
Task completed in 506ms
%FunPark.Monad.Either.Left{left: :not_found}
iex(61)> FunPark.Maintenance.Repo.add_schedule(apple)
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 501ms
Task completed in 501ms
Task completed in 501ms
%FunPark.Monad.Either.Right{
right: %FunPark.Ride{
id: 3842,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
}
iex(62)> FunPark.Monad.Effect.run(effect, env)
Task completed in 0ms
Task completed in 0ms
Task completed in 505ms
Task completed in 505ms
Task completed in 505ms
%FunPark.Monad.Either.Right{
right: %FunPark.Ride{
id: 3842,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
}
We made a Ride and then the env, once that was done we tried to get the Ride and it didn’t work then we added the Ride, and the Effect worked. Now let’s add an ability to check against all the tables.
# lib/fun_park/maintenance.ex
def check_in_all(%Ride{} = ride) do
ride
|> Repo.in_schedule()
|> bind(&Repo.in_unschedule/1)
|> bind(&Repo.in_lockout/1)
|> bind(&Repo.in_compliance/1)
|> Effect.run(%{store: Store})
end
Run It
iex(63)> FunPark.Maintenance.Repo.create_store()
%FunPark.Monad.Either.Left{
left: [
%ArgumentError{
message: "errors were found at the given arguments:\n\n * 1st argument: table name already exists\n"
},
%ArgumentError{
message: "errors were found at the given arguments:\n\n * 1st argument: table name already exists\n"
},
%ArgumentError{
message: "errors were found at the given arguments:\n\n * 1st argument: table name already exists\n"
},
%ArgumentError{
message: "errors were found at the given arguments:\n\n * 1st argument: table name already exists\n"
}
]
}
iex(64)> apple = FunPark.Ride.make("Apple Cart")
%FunPark.Ride{
id: 3906,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
iex(65)> FunPark.Maintenance.check_in_all(apple)
Task completed in 0ms
Task completed in 0ms
Task completed in 508ms
Task completed in 508ms
Task completed in 508ms
Task completed in 508ms
Task completed in 508ms
Task completed in 508ms
%FunPark.Monad.Either.Left{left: :not_found}
iex(66)> FunPark.Maintenance.add_to_all(apple)
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 501ms
Task completed in 501ms
Task completed in 501ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 526ms
Task completed in 526ms
Task completed in 526ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 503ms
Task completed in 504ms
Task completed in 504ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 521ms
Task completed in 521ms
Task completed in 521ms
%FunPark.Monad.Either.Right{
right: %FunPark.Ride{
id: 3906,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
}
iex(67)> FunPark.Maintenance.check_in_all(apple)
Task completed in 0ms
Task completed in 0ms
Task completed in 506ms
Task completed in 506ms
Task completed in 506ms
Task completed in 0ms
Task completed in 0ms
Task completed in 530ms
Task completed in 530ms
Task completed in 530ms
Task completed in 1036ms
Task completed in 0ms
Task completed in 0ms
Task completed in 502ms
Task completed in 502ms
Task completed in 502ms
Task completed in 1539ms
Task completed in 0ms
Task completed in 0ms
Task completed in 508ms
Task completed in 509ms
Task completed in 509ms
Task completed in 2048ms
%FunPark.Monad.Either.Right{
right: %FunPark.Ride{
id: 3906,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
}
iex(68)> FunPark.Maintenance.Repo.remove_lockout(apple)
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 510ms
Task completed in 510ms
Task completed in 510ms
Task completed in 510ms
%FunPark.Monad.Either.Right{
right: %FunPark.Ride{
id: 3906,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
}
iex(69)> FunPark.Maintenance.check_in_all(apple)
Task completed in 0ms
Task completed in 0ms
Task completed in 536ms
Task completed in 536ms
Task completed in 536ms
Task completed in 0ms
Task completed in 0ms
Task completed in 501ms
Task completed in 501ms
Task completed in 501ms
Task completed in 1037ms
Task completed in 0ms
Task completed in 0ms
Task completed in 517ms
Task completed in 517ms
Task completed in 517ms
Task completed in 1554ms
Task completed in 1554ms
%FunPark.Monad.Either.Left{left: :not_found}
We make a Ride check to see, then add the Ride then see if passes, then delete the Ride then see the failure again.
Flip the Logic
The Ride expert is contacting us again and wants to remind us that a Ride is considered online only with its not in any of the tables for maintenance. So we would like to have a Right returned if its not in any of the tables. So we can do some reverse logic and make that happen.
# lib/fun_park/maintenance/repo.ex
def assert_absent_effect(%Ride{} = ride, kleisli_fn, reason_msg) do
ride
|> kleisli_fn.()
|> Effect.flip_either()
|> bind(right_if_absent(ride))
|> Effect.map_left(replace_ride_with_reason(reason_msg))
end
defp right_if_absent(ride) do
fn
:not_found -> Effect.right(ride)
other -> Effect.left(other)
end
end
defp replace_ride_with_reason(reason_msg) do
fn
%Ride{} -> reason_msg
other -> other
end
end
def not_in_schedule(%Ride{} = ride) do
assert_absent_effect(
ride,
&in_schedule/1,
"#{ride.name} is in scheduled maintenance"
)
end
def not_in_unschedule(%Ride{} = ride) do
assert_absent_effect(
ride,
&in_unschedule/1,
"#{ride.name} is in unscheduled maintenance"
)
end
def not_in_lockout(%Ride{} = ride) do
assert_absent_effect(
ride,
&in_lockout/1,
"#{ride.name} is locked out"
)
end
def not_in_compliance(%Ride{} = ride) do
assert_absent_effect(
ride,
&in_compliance/1,
"#{ride.name} is out of compliance"
)
end
# lib/fun_park/maintenance.ex
def check_online_bind(%Ride{} = ride) do
ride
|> Repo.not_in_schedule()
|> bind(&Repo.not_in_unschedule/1)
|> bind(&Repo.not_in_lockout/1)
|> bind(&Repo.not_in_compliance/1)
|> Effect.run(%{store: Store})
end
Run It
iex(70)> FunPark.Maintenance.Repo.create_store()
%FunPark.Monad.Either.Left{
left: [
%ArgumentError{
message: "errors were found at the given arguments:\n\n * 1st argument: table name already exists\n"
},
%ArgumentError{
message: "errors were found at the given arguments:\n\n * 1st argument: table name already exists\n"
},
%ArgumentError{
message: "errors were found at the given arguments:\n\n * 1st argument: table name already exists\n"
},
%ArgumentError{
message: "errors were found at the given arguments:\n\n * 1st argument: table name already exists\n"
}
]
}
iex(71)> apple = FunPark.Ride.make("Apple Cart")
%FunPark.Ride{
id: 3970,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
iex(72)> FunPark.Maintenance.remove_from_all(apple)
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 511ms
Task completed in 513ms
Task completed in 513ms
Task completed in 513ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 501ms
Task completed in 501ms
Task completed in 501ms
Task completed in 501ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 511ms
Task completed in 511ms
Task completed in 511ms
Task completed in 511ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 502ms
Task completed in 502ms
Task completed in 502ms
Task completed in 502ms
%FunPark.Monad.Either.Right{
right: %FunPark.Ride{
id: 3970,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
}
iex(73)> FunPark.Maintenance.check_online_bind(apple)
Task completed in 0ms
Task completed in 0ms
Task completed in 509ms
Task completed in 509ms
Task completed in 509ms
Task completed in 509ms
Task completed in 0ms
Task completed in 509ms
Task completed in 0ms
Task completed in 0ms
Task completed in 501ms
Task completed in 501ms
Task completed in 501ms
Task completed in 501ms
Task completed in 0ms
Task completed in 501ms
Task completed in 1010ms
Task completed in 0ms
Task completed in 0ms
Task completed in 505ms
Task completed in 505ms
Task completed in 505ms
Task completed in 505ms
Task completed in 0ms
Task completed in 505ms
Task completed in 1515ms
Task completed in 0ms
Task completed in 0ms
Task completed in 501ms
Task completed in 501ms
Task completed in 501ms
Task completed in 501ms
Task completed in 0ms
Task completed in 501ms
Task completed in 2016ms
%FunPark.Monad.Either.Right{
right: %FunPark.Ride{
id: 3970,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
}
iex(74)> FunPark.Maintenance.add_to_all(apple)
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 516ms
Task completed in 516ms
Task completed in 516ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 502ms
Task completed in 502ms
Task completed in 502ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 551ms
Task completed in 551ms
Task completed in 551ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 507ms
Task completed in 507ms
Task completed in 507ms
%FunPark.Monad.Either.Right{
right: %FunPark.Ride{
id: 3970,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
}
iex(75)> FunPark.Maintenance.check_online_bind(apple)
Task completed in 0ms
Task completed in 0ms
Task completed in 507ms
Task completed in 507ms
Task completed in 507ms
Task completed in 507ms
Task completed in 507ms
Task completed in 507ms
Task completed in 507ms
Task completed in 507ms
%FunPark.Monad.Either.Left{left: "Apple Cart is in scheduled maintenance"}
Create the tables, see that the Ride is online, add the Ride to all the tables then see that the Ride is offline with a reason.
We now see that at any failure we are returning a Left and getting out. We want to now use the tables to check for online status of a Ride, this assumes that the tables are running at the time of the check. Let’s update the functions.
# ride.ex
def online?(%__MODULE__{online: online}), do: online
# maintenance.ex
def online?(%Ride{} = ride) do
ride
|> check_online()
|> Either.right?()
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
# maintenance.ex
def ensure_online(%Ride{} = ride) do
ride
|> check_online()
|> Either.map_left(&ValidationError.new/1)
end
We can now check the status of a ride based off the table status of a Ride
All Major Theme Parks Have Delays
We now notice an uptick of complaints about the sluggishness of the guest experience. This is caused by the the fact that we are running the checks in sequence and we don’t need to.
def check_online(%Ride{} = ride) do
Effect.validate(ride, [
&Repo.not_in_schedule/1,
&Repo.not_in_unschedule/1,
&Repo.not_in_lockout/1,
&Repo.not_in_compliance/1
])
|> Effect.run(%{store: Store})
end
Here is a table that will help with what each Effect function can do.
| Operation | Execution Style | Description |
|---|---|---|
| Sequential | Sequential |
Chain computations using bind |
| Sequential | Sequential |
Monadic sequence using traverse/2 |
| Parallel | Sequential |
Applicative sequence using traverse_a/2 |
Run It
iex(76)> FunPark.Maintenance.remove_from_all(apple)
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 503ms
Task completed in 503ms
Task completed in 503ms
Task completed in 503ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 501ms
Task completed in 501ms
Task completed in 501ms
Task completed in 501ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 503ms
Task completed in 503ms
Task completed in 503ms
Task completed in 503ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 510ms
Task completed in 510ms
Task completed in 510ms
Task completed in 510ms
%FunPark.Monad.Either.Right{
right: %FunPark.Ride{
id: 3970,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
}
iex(77)> FunPark.Maintenance.check_online(apple)
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 508ms
Task completed in 508ms
Task completed in 508ms
Task completed in 508ms
Task completed in 508ms
Task completed in 508ms
Task completed in 508ms
Task completed in 508ms
Task completed in 508ms
Task completed in 508ms
Task completed in 508ms
Task completed in 508ms
Task completed in 508ms
Task completed in 508ms
Task completed in 508ms
Task completed in 508ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 508ms
Task completed in 508ms
Task completed in 508ms
Task completed in 508ms
Task completed in 508ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 508ms
Task completed in 508ms
%FunPark.Monad.Either.Right{
right: %FunPark.Ride{
id: 3970,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
}
iex(78)> FunPark.Maintenance.add_to_all(apple)
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 501ms
Task completed in 501ms
Task completed in 501ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 507ms
Task completed in 507ms
Task completed in 507ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 501ms
Task completed in 502ms
Task completed in 502ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 509ms
Task completed in 509ms
Task completed in 509ms
%FunPark.Monad.Either.Right{
right: %FunPark.Ride{
id: 3970,
name: "Apple Cart",
min_age: 0,
min_height: 0,
wait_time: 0,
online: true,
tags: []
}
}
iex(79)> FunPark.Maintenance.check_online(apple)
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 550ms
Task completed in 550ms
Task completed in 550ms
Task completed in 550ms
Task completed in 550ms
Task completed in 550ms
Task completed in 550ms
Task completed in 550ms
Task completed in 550ms
Task completed in 550ms
Task completed in 550ms
Task completed in 550ms
Task completed in 550ms
Task completed in 550ms
Task completed in 550ms
Task completed in 550ms
Task completed in 550ms
Task completed in 550ms
Task completed in 550ms
Task completed in 550ms
Task completed in 550ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 550ms
Task completed in 550ms
%FunPark.Monad.Either.Left{
left: ["Apple Cart is out of compliance", "Apple Cart is locked out",
"Apple Cart is in unscheduled maintenance",
"Apple Cart is in scheduled maintenance"]
}
Now we hear that we should not even worry about the status of a ride unless the Patron is eligible for that Ride
def validate_fast_pass_lane_b(%Patron{} = patron, %Ride{} = ride) do
validate_vip_or_pass = curry(&ensure_vip_or_fast_pass/2)
validate_eligibility = curry(&validate_eligibility/2)
Either.validate(
ride,
[
validate_eligibility.(patron),
validate_vip_or_pass.(patron)
]
)
|> bind(&Maintenance.ensure_online/1)
|> map(fn _ -> patron end)
end
Bind will run all the checks at once and will run all the expensive checks only after a Patron is eligible.
Run It
iex(80)> FunPark.Maintenance.Repo.create_store()
%FunPark.Monad.Either.Left{
left: [
%ArgumentError{
message: "errors were found at the given arguments:\n\n * 1st argument: table name already exists\n"
},
%ArgumentError{
message: "errors were found at the given arguments:\n\n * 1st argument: table name already exists\n"
},
%ArgumentError{
message: "errors were found at the given arguments:\n\n * 1st argument: table name already exists\n"
},
%ArgumentError{
message: "errors were found at the given arguments:\n\n * 1st argument: table name already exists\n"
}
]
}
iex(81)> beth = FunPark.Patron.make("Beth", 16, 115)
%FunPark.Patron{
id: 4034,
name: "Beth",
age: 16,
height: 115,
ticket_tier: :basic,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(82)> elsie = FunPark.Patron.make("Elsie", 17, 135, ticket_tier: :vip)
%FunPark.Patron{
id: 4098,
name: "Elsie",
age: 17,
height: 135,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
iex(83)> haunted_mansion = FunPark.Ride.make("Haunted Mansion", min_age: 14, min_height: 120)
%FunPark.Ride{
id: 4162,
name: "Haunted Mansion",
min_age: 14,
min_height: 120,
wait_time: 0,
online: true,
tags: []
}
iex(84)> FunPark.Ride.FastLane.validate_fast_pass_lane_b(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(85)> FunPark.Ride.FastLane.validate_fast_pass_lane_b(elsie, haunted_mansion)
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 506ms
Task completed in 506ms
Task completed in 506ms
Task completed in 506ms
Task completed in 507ms
Task completed in 507ms
Task completed in 507ms
Task completed in 507ms
Task completed in 507ms
Task completed in 507ms
Task completed in 507ms
Task completed in 507ms
Task completed in 507ms
Task completed in 507ms
Task completed in 507ms
Task completed in 507ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 507ms
Task completed in 507ms
Task completed in 507ms
Task completed in 507ms
Task completed in 507ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 507ms
Task completed in 507ms
%FunPark.Monad.Either.Right{
right: %FunPark.Patron{
id: 4098,
name: "Elsie",
age: 17,
height: 135,
ticket_tier: :vip,
fast_passes: [],
reward_points: 0,
likes: [],
dislikes: []
}
}
iex(86)> FunPark.Maintenance.Repo.add_lockout(haunted_mansion)
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 508ms
Task completed in 508ms
Task completed in 508ms
%FunPark.Monad.Either.Right{
right: %FunPark.Ride{
id: 4162,
name: "Haunted Mansion",
min_age: 14,
min_height: 120,
wait_time: 0,
online: true,
tags: []
}
}
iex(87)> FunPark.Ride.FastLane.validate_fast_pass_lane_b(elsie, haunted_mansion)
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 518ms
Task completed in 518ms
Task completed in 519ms
Task completed in 519ms
Task completed in 519ms
Task completed in 519ms
Task completed in 519ms
Task completed in 519ms
Task completed in 519ms
Task completed in 519ms
Task completed in 519ms
Task completed in 519ms
Task completed in 519ms
Task completed in 519ms
Task completed in 519ms
Task completed in 519ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 519ms
Task completed in 519ms
Task completed in 519ms
Task completed in 519ms
Task completed in 519ms
Task completed in 0ms
Task completed in 0ms
Task completed in 0ms
Task completed in 519ms
Task completed in 519ms
%FunPark.Monad.Either.Left{
left: %FunPark.Errors.ValidationError{
errors: ["Haunted Mansion is locked out"]
}
}
So we can see that we only run harder checks if the Patron is eligible for the _Ride.
What You’ve Learned
This is the end of the book. There is a lot of functionality that we gained while reading through this book. I would now suggest that you start to go over all the notes that you have and see how far you have come.
You have learned how to take simple functions and put them together into more and more complex functions. curry is one that will come in handy as you use these frameworks going forward. Either is a great way to handle different states and Effect is a great way to handle errors.
I don’t know if this book will make it into every aspect of my coding journey but I will keep all these lessons.