Skuld.Effects.Yield (skuld v0.1.13)

View Source

Yield effect - coroutine-style suspension and resumption.

Uses %Skuld.Comp.Suspend{} struct as the suspension result, which bypasses leave_scope in Run.

Architecture

  • yield(value) suspends computation, returning %Suspend{value, resume}
  • The resume function captures the env, so caller just provides input
  • Run recognizes %Suspend{} and bypasses leave_scope
  • When resumed, the result goes through the leave_scope chain

Summary

Functions

Collect all yielded values until completion.

Feed a list of inputs to a computation, collecting yields.

Handler: returns Suspend struct with resume that captures env and invokes leave_scope when the resumed computation completes.

Intercept yields from a computation and respond to them.

Run a computation with a driver function that handles yields.

Install a scoped Yield handler for a computation.

Yield without a value

Yield a value and suspend, waiting for input to resume

Functions

collect(comp, resume_input \\ nil)

@spec collect(Skuld.Comp.Types.computation(), term()) ::
  {:done, term(), [term()], Skuld.Comp.Types.env()}
  | {:thrown, term(), [term()], Skuld.Comp.Types.env()}

Collect all yielded values until completion.

Resumes with the provided input value (default: nil) each time.

The computation should already have handlers installed via with_handler.

feed(comp, inputs)

@spec feed(Skuld.Comp.Types.computation(), [term()]) ::
  {:done, term(), [term()], Skuld.Comp.Types.env()}
  | {:suspended, term(),
     (term() -> {Skuld.Comp.Types.result(), Skuld.Comp.Types.env()}), [term()],
     Skuld.Comp.Types.env()}
  | {:thrown, term(), [term()], Skuld.Comp.Types.env()}

Feed a list of inputs to a computation, collecting yields.

Each yield consumes one input. If inputs run out, stops with remaining computation.

The computation should already have handlers installed via with_handler.

handle(yield, env, k)

Handler: returns Suspend struct with resume that captures env and invokes leave_scope when the resumed computation completes.

respond(inner_comp, responder)

Intercept yields from a computation and respond to them.

Similar to Throw.catch_error/2, but for yields instead of throws. The responder function receives the yielded value and returns a computation that produces the resume value. If the responder re-yields (calls Yield.yield), that yield propagates to the outer handler.

Implementation Note

This implementation wraps the Yield handler in the Env to intercept yields directly at the handler level. A previous implementation used a different approach: replacing the leave_scope with an identity function, running the inner computation to completion, then pattern matching on %Suspend{} results to detect yields. That approach was more complex (~180 lines vs ~80 lines), required manual env state merging, and interfered with other effects that rely on leave_scope (such as EffectLogger).

Example

# Handle all yields internally:
comp do
  result <- Yield.respond(
    comp do
      x <- Yield.yield(:get_value)
      y <- Yield.yield(:get_another)
      x + y
    end,
    fn
      :get_value -> Comp.pure(10)
      :get_another -> Comp.pure(20)
    end
  )
  result
end
|> Yield.with_handler()
|> Comp.run!()
#=> 30

# Responder can use effects:
comp do
  result <- Yield.respond(
    comp do
      x <- Yield.yield(:get_state)
      x * 2
    end,
    fn :get_state -> State.get() end
  )
  result
end
|> State.with_handler(21)
|> Yield.with_handler()
|> Comp.run!()
#=> 42

# Unhandled yields propagate (re-yield):
comp do
  result <- Yield.respond(
    comp do
      x <- Yield.yield(:handled)
      y <- Yield.yield(:not_handled)
      x + y
    end,
    fn
      :handled -> Comp.pure(10)
      other -> Yield.yield(other)  # re-yield to outer handler
    end
  )
  result
end
|> Yield.with_handler()
|> Comp.run()
# Returns %Suspend{value: :not_handled, ...}

run_with_driver(comp, driver)

@spec run_with_driver(
  Skuld.Comp.Types.computation(),
  (value :: term(), data :: map() | nil ->
     {:continue, term()} | {:stop, term()})
) ::
  {:done, term(), Skuld.Comp.Types.env()}
  | {:stopped, term(), Skuld.Comp.Types.env()}
  | {:thrown, term(), Skuld.Comp.Types.env()}

Run a computation with a driver function that handles yields.

The driver receives yielded values and returns {:continue, input} or {:stop, reason}.

The computation should already have handlers installed via with_handler.

with_handler(comp)

Install a scoped Yield handler for a computation.

Installs the Yield handler for the duration of comp. The handler is restored/removed when comp completes or suspends.

The argument order is pipe-friendly.

Example

# Wrap a computation with Yield handling
comp_with_yield =
  comp do
    input <- Yield.yield(:question)
    return({:got, input})
  end
  |> Yield.with_handler()

# Compose with other handlers
my_comp
|> Yield.with_handler()
|> State.with_handler(0)
|> Comp.run(Env.new())

yield()

@spec yield() :: Skuld.Comp.Types.computation()

Yield without a value

yield(value)

@spec yield(term()) :: Skuld.Comp.Types.computation()

Yield a value and suspend, waiting for input to resume