Home Posts Post Search Tag Search

Advanced Functional Elixir - 10 - Coordinate Tasks with Effect
Published on: 2026-04-26 Tags: elixir, Blog, Advanced Functional Programming, FunPark, Monoids, predicates, Monads, Effect

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.