Pull[O, R] represents a computation that emits zero or more output values of type O, then terminates with one result of type R. Unlike a collection, it makes the final result explicit: output production and computation sequencing are separate concerns. A pull is evaluated incrementally with step, which either returns the final result or one output together with the remaining pull. fold repeatedly steps a pull to consume its outputs. section 15.2
Listing 15.2 The Pull data type
enum Pull[+O, +R]:
case Result[+R](result: R) extends Pull[Nothing, R]
case Output[+O](value: O) extends Pull[O, Unit]
case FlatMap[X, +O, +R](
source: Pull[O, X],
f: X => Pull[O, R]) extends Pull[O, R]
def step: Either[R, (O, Pull[O, R])] = this match #1
case Result(r) => Left(r)
case Output(o) => Right(o, Pull.done)
case FlatMap(source, f) =>
source match
case FlatMap(s2, g) =>
s2.flatMap(x => g(x).flatMap(y => f(y))).step
case other => other.step match
case Left(r) => f(r).step
case Right((hd, tl)) => Right((hd, tl.flatMap(f)))
@annotation.tailrec
final def fold[A](init: A)(f: (A, O) => A): (R, A) = #2
step match
case Left(r) => (r, init)
case Right((hd, tl)) => tl.fold(f(init, hd))(f)
def toList: List[O] = #3
fold(List.newBuilder[O])((bldr, o) => bldr += o)(1).result
def flatMap[O2 >: O, R2](
f: R => Pull[O2, R2]
): Pull[O2, R2] = #4
Pull.FlatMap(this, f)
def >>[O2 >: O, R2](next: => Pull[O2, R2]): Pull[O2, R2] =
flatMap(_ => next)
def map[R2](f: R => R2]): Pull[O, R2] =
flatMap(r => Result(f(r)))
The core representation distinguishes completion, one output, and deferred continuation. step reassociates nested FlatMap values while exposing one output at a time; fold consumes those steps. section 15.2
Output and Result are the basic building blocks. Output(o) emits o and has a Unit result; Result(r) ends without output. flatMap sequences pulls through their final result R rather than transforming each emitted value. Consequently, >>—shorthand for flatMap(_ => next)—concatenates output-producing pulls while preserving deferred evaluation. Covariant output and result parameters allow Result and Output to use Nothing where they produce no output or never supply a compatible result. section 15.2 section 15.2.1
Construct and inspect pulls with recursive, incremental combinators:
- Build finite pulls from List or LazyList by emitting each head and lazily sequencing the recursive tail; use unfold to factor this recurring pattern into a seed and one-step function. section 15.2.1
- Represent nonterminating production with continually, repeat, or iterate; their result type is Nothing because they never complete normally. section 15.2.1
- Partially evaluate a pull with take, which stops after a requested number of outputs and retains an optional source result when termination was reached. section 15.2.1
- Transform outputs independently of completion using mapOutput, filter, count, tally, or mapAccumulate; use uncons when a transformation needs the next step as an explicit value. section 15.2.1
Duplicating an infinite stream
Starting with Pull.iterate(0)(_ + 1), flatMapOutput can turn each emitted integer into a pull containing that integer twice. Taking ten outputs yields 0, 0, 1, 1, 2, 2, 3, 3, 4, 4. This contrasts output-level transformation with Pull.flatMap, whose continuation receives only a pull’s final result. section 15.2.1
Stream[O] is an opaque view of Pull[O, Unit] that emphasizes list-like operations on emitted elements. Pull remains useful when a transformation needs to describe constituent computations and their terminal results; Stream provides element-oriented map and flatMap behavior. A Pipe[I, O] is simply a function from Stream[I] to Stream[O], so independently named transformations compose with ordinary function composition. section 15.2.1 section 15.2.2
Pull can be extended from pure lazy computation to effectful streaming as Pull[F, O, R]. Eval wraps an F[R] action without emitting an output, while step and fold require a Monad to interpret effects. Error handlers can be represented within Pull and interpreted through MonadThrow. Resource scopes add finalizers so acquired resources are released after normal completion, early termination, or unhandled failure. section 15.3 section 15.3.2 section 15.3.3
Resource-safe file conversion
A stream can acquire a file or writer with resource, emit lines through effectful evaluation, apply a composed conversion Pipe, and write output lines. The associated finalizers close the resources even when the stream ends early or fails. section 15.3.3 section 15.6
