liveBook · Manning

Pull

As explained in Functional Programming in Scala, Second Edition

Pull is a book-defined data type representing a computation that emits zero or more values of one type and then terminates with a result value of another type.

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

How Functional Programming in Scala, Second Edition covers it

How deep it goes
Develops constructors, stepping, recursion, output combinators, effects, errors, and scoped resource finalization.
Examples
Uses lists, lazy lists, infinite integers, normalized strings, and resource-safe file conversion.
What it assumes
Builds from concrete List and LazyList recursion before generalizing to monadic recursion and type-class constraints.
What you can do after
Build it from scratch as incremental, composable streaming computations with effects, errors, and finalizers.
How it connects
Connects Pull with Stream, LazyList, Monad, Pipe, MonadThrow, and resource safety.

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Functional Programming in Scala, Second Edition. The section links point to the parts of the book it draws on.