Skip to main content

lib/skuld/effects/fiber_yield.ex

defmodule Skuld.Effects.FiberYield do
  @moduledoc false

  use Skuld.Comp.DefOp

  alias Skuld.Comp
  alias Skuld.Comp.InternalSuspend

  @yield_op Module.concat(Skuld.Effects.Yield, Yield)

  def_op notify(value)

  @doc """
  Install a Yield handler that produces `InternalSuspend` with `FiberYield`
  payloads instead of `ExternalSuspend`, and a notify handler for
  fire-and-forget notifications.

  Use inside a FiberPool context so the scheduler can manage suspended
  fibers while other fibers run.

      computation
      |> FiberYield.with_handler()
      |> FiberPool.with_handler()
      |> Comp.run()
  """
  @spec with_handler(Comp.Types.computation()) :: Comp.Types.computation()
  def with_handler(computation) do
    computation
    |> Comp.with_handler(Skuld.Effects.Yield, &handle_yield/3)
    |> Comp.with_handler(__MODULE__, &handle_notify/3)
  end

  @doc false
  def handle_yield({@yield_op, value}, env, k) do
    resume = fn input, resume_env ->
      {result, final_env} = k.(input, resume_env)

      case result do
        %InternalSuspend{} -> {result, final_env}
        %Comp.ExternalSuspend{} -> {result, final_env}
        _ -> Comp.Env.run_leave_scope(final_env, result)
      end
    end

    suspend = InternalSuspend.fiber_yield(value, resume)
    {suspend, env}
  end

  @notify_op @__notify_op__

  @doc false
  def handle_notify({@notify_op, value}, env, k) do
    resume = fn _input, resume_env -> k.(nil, resume_env) end
    suspend = InternalSuspend.fiber_notify(value, resume)
    {suspend, env}
  end
end