Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Sequence

The one lazy iteration protocol: pull-based sequences with natural names.

A Sequence is a pull producer: a thunk that, when forced with (()), yields one Step – either SDone (exhausted) or SMore(x, rest), the next element x paired with the thunk producing the remainder. Nothing is materialized up front; each (()) advances the stream by one element. A transformer (map, filter, take, …) wraps a producer in a new producer that pulls, reshapes, and re-yields; a consumer (fold, sum, to_list, …) drives the producer to exhaustion and folds the elements into a value.

Type

type Step(a) = SDone | SMore(a, (Unit) -> Step(a))
Seq(a)       = (Unit) -> Step(a)          -- read `Seq(a)` in the prose as this

The step continuation is ordinary data, not an effect, so every combinator is a total pure function over Step. That is the whole reason this substrate exists: an earlier push design routed elements through a shared Emit effect, and a single Emit(a) label per scope made element-type changes, heterogeneous zip, and mixing two element types in one function unrepresentable. On pull Step the element type is a plain type parameter, so map : Seq(a) -> Seq(b) type-changes freely, zip : (Seq(a), Seq(b)) -> Seq((a, b)) is genuinely heterogeneous, and a consumer’s per-element function may perform any ambient effect (it runs directly, with no handler closing the row over it). Every combinator below exercises one of those, and the correctness corpus in tests/cases/run pins them on both backends.

Effect-polymorphic consumers

for_each’s function argument runs in the ambient row: for_each(s, \(x) -> println(x)) threads IO straight out with no annotation, because the driver is a plain recursion and installs no handler. This is the effectful-mapper case the push substrate could not express.

Naming and the import idiom

The combinators keep their natural names (map, filter, take, zip, …), which collide with the prelude’s eager List surface. That is why this module is opt-in and NOT opened by the prelude: the flat namespace has no shadowing, so the documented idiom is a qualified import at the use site:

import Sequence as Seq          -- qualified: Seq.map, Seq.filter, Seq.take
Seq.to_list(Seq.filter(Seq.map(Seq.range(1, 100), \(x) -> x * x), even))

A selective import Sequence (unfold, iterate) also works for names that do not clash with the prelude.

Fusion status (baseline: unfused)

A pull pipeline is correct and constant-space per stage, but on the current optimizer it is NOT yet fused: each transformer stage allocates one SMore cons and one step thunk per element, so a k-stage pipeline over n elements allocates ~2kn heap cells (PRISM_ALLOC_STATS counts them; the algebraic-effect EOp counter reads zero here because a pull sequence performs no effects). The tests/perf_gate.rs pull-allocation cases pin those current numbers as the baseline; a dedicated stream-fusion pass (Coutts-style stream/unstream with case-of-case) is the separate work that ratchets them toward O(1). For contrast, the eager Data.List surface already fuses to ~1 cell per element with FBIP reuse recycling the uniquely-owned spine, so it is the faster tier today when strict, non-incremental traversal is acceptable; pull Sequence earns its keep on laziness (early exit through take/find, infinite producers) rather than on allocation, until fusion lands.

Types

Step

type Step(a) = SDone | SMore(a, (Unit) -> Step(a))

Functions and Values

empty

empty : forall a. () -> (Unit) -> Sequence.Step(a)

The empty sequence: yields nothing.

singleton

singleton : forall a. (a) -> (Unit) -> Sequence.Step(a)

The one-element sequence yielding x.

range

range : (Int, Int) -> (Unit) -> Sequence.Step(Int)

The ascending integers in [lo, hi). Self-recursive: the continuation is the next range, so no element is built until pulled.

from_list

from_list : forall a. (List(a)) -> (Unit) -> Sequence.Step(a)

The container boundary in: the elements of list xs, in order.

iterate

iterate : forall a. (a, (a) -> a) -> (Unit) -> Sequence.Step(a)

The infinite sequence x, f(x), f(f(x)), .... Bound it with take.

repeat

repeat : forall a. (a) -> (Unit) -> Sequence.Step(a)

The infinite sequence of x repeated. Bound it with take.

unfold

unfold : forall a b. (a, (a) -> Option((b, a))) -> (Unit) -> Sequence.Step(b)

The generator producer: yield x for each Some((x, seed')) that step returns from the running seed, stopping at None. Every finite producer is a special case; on pull Step a generator is just a fold over the seed, needing no coroutine or handler.

map

map : forall a b. ((Unit) -> Sequence.Step(a), (a) -> b) -> (Unit) -> Sequence.Step(b)

Apply f to every element. Type-changing: Seq(a) -> Seq(b).

filter

filter : forall a. ((Unit) -> Sequence.Step(a), (a) -> Bool) -> (Unit) -> Sequence.Step(a)

Keep only the elements satisfying p.

filter_map

filter_map : forall a b. ((Unit) -> Sequence.Step(a), (a) -> Option(b)) -> (Unit) -> Sequence.Step(b)

Map and filter in one pass: yield y for each f(x) == Some(y), dropping the Nones.

append

append : forall a. ((Unit) -> Sequence.Step(a), (Unit) -> Sequence.Step(a)) -> (Unit) -> Sequence.Step(a)

Concatenate two sequences: all of s, then all of t.

flat_map

flat_map : forall a b. ((Unit) -> Sequence.Step(a), (a) -> (Unit) -> Sequence.Step(b)) -> (Unit) -> Sequence.Step(b)

For each element x, splice in the sequence f(x).

take

take : forall a. ((Unit) -> Sequence.Step(a), Int) -> (Unit) -> Sequence.Step(a)

The first n elements, stopping the producer once the budget is spent.

drop

drop : forall a. ((Unit) -> Sequence.Step(a), Int) -> (Unit) -> Sequence.Step(a)

Skip the first n elements, yielding the rest.

take_while

take_while : forall a. ((Unit) -> Sequence.Step(a), (a) -> Bool) -> (Unit) -> Sequence.Step(a)

The longest prefix whose elements satisfy p; stops at the first failure.

drop_while

drop_while : forall a. ((Unit) -> Sequence.Step(a), (a) -> Bool) -> (Unit) -> Sequence.Step(a)

Drop the longest prefix satisfying p, yielding the rest.

dedup

dedup : forall e1 a. ((Unit) -> Sequence.Step(Int) ! {e1}) -> (a) -> Sequence.Step(Int) ! {e1}

Drop consecutive duplicates, keeping the first of each run (needs Eq(a)). Left unsigned so the Eq(a) constraint is inferred from ==.

enumerate

enumerate : forall a. ((Unit) -> Sequence.Step(a)) -> (Unit) -> Sequence.Step((Int, a))

Pair each element with its zero-based index, yielding (i, x).

scan

scan : forall a b. ((Unit) -> Sequence.Step(a), b, (b, a) -> b) -> (Unit) -> Sequence.Step(b)

The running left-fold, streamed: yield z, then each successive accumulator.

interleave

interleave : forall a. ((Unit) -> Sequence.Step(a), (Unit) -> Sequence.Step(a)) -> (Unit) -> Sequence.Step(a)

Alternate elements of s and t (s0, t0, s1, t1, ...); when one runs out, yield the whole remainder of the other. Streamed, no materialization.

zip

zip : forall a b. ((Unit) -> Sequence.Step(a), (Unit) -> Sequence.Step(b)) -> (Unit) -> Sequence.Step((a, b))

Pair two sequences element-wise, stopping at the shorter. Genuinely heterogeneous: Seq(a) and Seq(b) yield Seq((a, b)).

zip_with

zip_with : forall a b c. ((Unit) -> Sequence.Step(a), (Unit) -> Sequence.Step(b), (a, b) -> c) -> (Unit) -> Sequence.Step(c)

Combine two sequences element-wise with f, stopping when either runs out. Heterogeneous in both operand types.

chunk

chunk : forall a. ((Unit) -> Sequence.Step(a), Int) -> (Unit) -> Sequence.Step(List(a))

Group elements into consecutive lists of length n (the final chunk may be shorter). Pulls one group at a time, holding only the current group.

window

window : forall a. ((Unit) -> Sequence.Step(a), Int) -> (Unit) -> Sequence.Step(List(a))

Every contiguous length-n sliding window, in order. Fewer than n elements yields nothing. Holds one window (n elements) at a time.

fold

fold : forall a b. ((Unit) -> Sequence.Step(b), a, (a, b) -> a) -> a

Left-fold the sequence with f from initial accumulator z.

for_each

for_each : forall e0 a. ((Unit) -> Sequence.Step(a), (a) -> Unit ! {e0}) -> Unit ! {e0}

Run f for its effects on each element, in order. Effect-polymorphic: f runs in the ambient row (no handler intervenes), so for_each(s, \(x) -> println(x)) threads IO out. The explicit {| e} row matters: unsigned, the self-recursion would infer f as pure and reject an effectful body.

sum

sum : ((Unit) -> Sequence.Step(Int)) -> Int

Sum a sequence of integers.

product

product : ((Unit) -> Sequence.Step(Int)) -> Int

Product of a sequence of integers.

count

count : forall a. ((Unit) -> Sequence.Step(a)) -> Int

The number of elements.

to_list

to_list : forall a. ((Unit) -> Sequence.Step(a)) -> List(a)

Collect the sequence into a list, in order. The container boundary out.

head : forall e0 a. ((Unit) -> Sequence.Step(a) ! {e0}) -> Option(a) ! {e0}

The first element, or None if the sequence is empty.

find

find : forall a. ((Unit) -> Sequence.Step(a), (a) -> Bool) -> Option(a)

The first element satisfying p, or None; stops the producer at the match.

any

any : forall a. ((Unit) -> Sequence.Step(a), (a) -> Bool) -> Bool

True when some element satisfies p; short-circuits.

all

all : forall a. ((Unit) -> Sequence.Step(a), (a) -> Bool) -> Bool

True when every element satisfies p; short-circuits on the first failure.

from_bytes

from_bytes : (Wire.Bytes) -> (Unit) -> Sequence.Step(Int)

The bytes of bs as a sequence of ints in 0..255, in order.

to_bytes

to_bytes : ((Unit) -> Sequence.Step(Int)) -> Wire.Bytes

Collect a sequence of ints (each masked to a byte) into a Bytes. The Bytes container boundary out.