Yield
Yield is the immediate result returned by a Stage when it processes an input value.
Every Yield bundles three things together:
- an optional output value of type
O; - a
Statusdescribing how the stage completed; - an
Evolutionthat knows which stage to use when the pipeline is ready to process the next input.
Yield is not the final answer the pipeline gives to the outside world — that is Outcome.
Yield is the internal value that flows from one stage to the next, carrying the continuation forward.
import h8io.stages.*
Carrying a Value vs Carrying Nothing
There are exactly two variants.
Yield.Some is produced when the stage has a result to hand downstream. It carries the output value together with the
status and the evolution:
val some = Yield.Some(42, Status.Success, new Evolution[Int, Int, Nothing] {
override def evolve(status: Status[?]): Stage[Int, Int, Nothing] = ???
override def dispose(): Unit = ()
})
// some: Yield.Some[Int, Int, Nothing] = Some(
// out = 42,
// status = Success,
// evolution = repl.MdocSession$MdocApp$$anon$1@3db05149
// )
some.out
// res0: Int = 42
some.status
// res1: Status[Nothing] = Success
Yield.None is produced when the stage deliberately emits nothing — for example, a filter that drops a particular
input. There is no output value, but the status and the evolution are still present so the pipeline can continue
correctly:
val none = Yield.None[Int, String, String](Status.error("filtered out"), new Evolution[Int, String, String] {
override def evolve(status: Status[?]): Stage[Int, String, String] = ???
override def dispose(): Unit = ()
})
// none: Yield.None[Int, String, String] = None(
// status = Complete("filtered out"),
// evolution = repl.MdocSession$MdocApp$$anon$2@e5dbc46
// )
none.status
// res2: Status[String] = Complete("filtered out")
When a Yield.None reaches a Stage.AndThen, the downstream stage is not applied to the current input.
Instead it is wired into the evolution so that the full composed stage will be called when a value eventually
arrives.
Accessing the Output
outOption returns the output wrapped in Some, or None when no value was produced:
some.outOption // Some(42)
// res3: Option[Int] = Some(value = 42)
none.outOption // None
// res4: Option[String] = None
Use outOption when you only need to know whether a value was produced. When you also need the status or
evolution, pattern-match on the concrete subtype directly instead.
Evolving the Pipeline
Once the pipeline has processed an input and is ready for the next one, it calls evolve() on the Yield returned
by the last stage.
evolve() calls evolution.evolve(status) — the Evolution receives the current status and returns the next stage.
object DoubleStage extends Stage[Int, Int, Nothing] {
private def stub: Evolution[Int, Int, Nothing] = new Evolution[Int, Int, Nothing] {
override def evolve(status: Status[?]): Stage[Int, Int, Nothing] = ???
override def dispose(): Unit = ()
}
override def apply(in: Int): Yield[Int, Int, Nothing] =
Yield.Some(in * 2, Status.Success, stub)
override def skip(): Evolution[Int, Int, Nothing] = stub
}
object ErrorRecovery extends Stage[Int, Int, Nothing] {
private def stub: Evolution[Int, Int, Nothing] = new Evolution[Int, Int, Nothing] {
override def evolve(status: Status[?]): Stage[Int, Int, Nothing] = ???
override def dispose(): Unit = ()
}
override def apply(in: Int): Yield[Int, Int, Nothing] =
Yield.Some(0, Status.complete, stub)
override def skip(): Evolution[Int, Int, Nothing] = stub
}
val yldSuccess = Yield.Some(
21,
Status.Success,
new Evolution[Int, Int, Nothing] {
override def evolve(status: Status[?]): Stage[Int, Int, Nothing] = status match {
case Status.Success => DoubleStage
case _ => ErrorRecovery
}
override def dispose(): Unit = ()
})
// yldSuccess: Yield.Some[Int, Int, Nothing] = Some(
// out = 21,
// status = Success,
// evolution = repl.MdocSession$MdocApp$$anon$5@7605df9f
// )
val nextStage = yldSuccess.evolve()
// nextStage: Stage[Int, Int, Nothing] = <function1>
Because the status is Success, the evolution returns DoubleStage.
Applying it to a new input produces the expected result:
nextStage(21)
// res5: Yield[Int, Int, Nothing] = Some(
// out = 42,
// status = Success,
// evolution = repl.MdocSession$MdocApp$DoubleStage$$anon$3@35a02a07
// )
Transforming a Yield
map transforms all three components of a Yield in one step. Each component gets its own mapping function.
This is mainly used inside pipeline combinators rather than in application code, but it is part of the public API.
Imagine a stage that reads a temperature sensor and yields the value in Celsius. A downstream combinator
needs Fahrenheit: it converts the value, marks each successful reading as complete, and wraps the evolution
with Evolution.map so that future stages also go through the same conversion:
object ThermStage extends Stage[Int, Int, Nothing] {
private val evo: Evolution[Int, Int, Nothing] = new Evolution[Int, Int, Nothing] {
override def evolve(status: Status[?]): Stage[Int, Int, Nothing] = ThermStage
override def dispose(): Unit = ()
}
override def apply(in: Int): Yield[Int, Int, Nothing] = Yield.Some(25, Status.Success, evo)
override def skip(): Evolution[Int, Int, Nothing] = evo
}
object ToFahrenheit extends Stage[Int, Int, Nothing] {
private val evo: Evolution[Int, Int, Nothing] = new Evolution[Int, Int, Nothing] {
override def evolve(status: Status[?]): Stage[Int, Int, Nothing] = ToFahrenheit
override def dispose(): Unit = ()
}
override def apply(in: Int): Yield[Int, Int, Nothing] = Yield.Some(in * 9 / 5 + 32, Status.Success, evo)
override def skip(): Evolution[Int, Int, Nothing] = evo
}
val celsius = ThermStage(0)
// celsius: Yield[Int, Int, Nothing] = Some(
// out = 25,
// status = Success,
// evolution = repl.MdocSession$MdocApp$ThermStage$$anon$6@6fd94e10
// )
val fahrenheit = celsius.map(
mapOut = c => c * 9 / 5 + 32,
mapStatus = _ => Status.complete,
mapEvolution = evo => evo.map(_ ~> ToFahrenheit))
// fahrenheit: Yield[Int, Int, Nothing] = Some(
// out = 77,
// status = Complete(),
// evolution = Mapped(
// evolution = repl.MdocSession$MdocApp$ThermStage$$anon$6@6fd94e10,
// f = <function1>
// )
// )
fahrenheit.outOption
// res6: Option[Int] = Some(value = 77)
fahrenheit.status
// res7: Status[Nothing] = Complete()
For Yield.None, map applies mapStatus and mapEvolution as usual — mapOut is accepted for
type-consistency but is never invoked because there is no value to transform. Here the sensor went
offline before any reading was taken:
val noReading = Yield.None[Int, Int, String](Status.error("sensor offline"), ThermStage.skip())
.map(
mapOut = c => c * 9 / 5 + 32, // not called — no value
mapStatus = _ => Status.error("no data"),
mapEvolution = evo => evo.map(_ ~> ToFahrenheit))
// noReading: Yield[Int, Int, String] = None(
// status = Complete("no data"),
// evolution = Mapped(
// evolution = repl.MdocSession$MdocApp$ThermStage$$anon$6@6fd94e10,
// f = <function1>
// )
// )
noReading.outOption
// res8: Option[Int] = None
noReading.status
// res9: Status[String] = Complete("no data")