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

Weaves: Generators and Coroutines

Weaves are Seq’s implementation of generators/coroutines, built on top of the CSP-style strand system. They provide bidirectional communication between a producer and consumer through structured yield/resume semantics.

Why Weaves?

Generators are useful when you need to:

  • Produce values lazily (only compute when needed)
  • Maintain state between iterations without explicit data structures
  • Transform streams of data with backpressure
  • Implement query engines or interpreters that yield results incrementally

Seq’s weaves are unique in that they’re built on the same strand/channel infrastructure as CSP concurrency. A weave is essentially a strand with a structured protocol for yield and resume.

Basic Concepts

TermDescription
WeaveA suspended computation that yields values and receives resume values
HandleA WeaveCtx value used to resume and communicate with a weave
YieldPause execution, send a value to the caller, wait for a resume value
ResumeSend a value into a paused weave, receive its next yielded value

Core Operations

WordEffectDescription
strand.weave( Quotation -- WeaveCtx )Create a weave from a quotation
strand.resume( WeaveCtx T -- WeaveCtx T Bool )Resume with value, get (handle, yielded, has_more)
yield( Ctx T -- Ctx T | Yield T )Yield value, receive resume value
strand.weave-cancel( WeaveCtx -- )Cancel a weave and release resources

Simple Counter Example

A counter that yields its current value and accepts an increment:

# Counter - yields current count, receives increment
: counter ( Ctx Int -- | Yield Int )
  tuck           # ( count Ctx count )
  yield          # yield count, receive increment -> ( count Ctx increment )
  rot            # ( Ctx increment count )
  i.add          # ( Ctx new_count )
  counter        # tail recurse
;

: main ( -- )
  # Create weave
  [ counter ] strand.weave        # ( handle )

  # Resume with initial value 10
  10 strand.resume                # ( handle yielded has_more )
  drop                            # ( handle yielded )
  "First: " swap int->string string.concat io.write-line
                                  # ( handle )

  # Resume with increment 5
  5 strand.resume                 # ( handle yielded has_more )
  drop
  "After +5: " swap int->string string.concat io.write-line

  strand.weave-cancel             # Clean up (infinite generator)
;

Output:

First: 10
After +5: 15

The Ctx Threading Pattern

Critical: The weave context (Ctx) must be explicitly threaded through your code. This is different from languages where generator state is implicit.

The quotation passed to strand.weave receives (Ctx, first_resume_value) and must keep the Ctx accessible for yield calls:

# WRONG - loses the Ctx
: bad-generator ( Ctx Int -- | Yield Int )
  yield          # Error: Ctx is buried under Int
;

# CORRECT - Ctx is on top for yield
: good-generator ( Ctx Int -- | Yield Int )
  tuck           # ( Int Ctx Int )
  yield          # ( Int Ctx resume_value )
  ...
;

Handling Weave Completion

strand.resume returns ( handle value has_more ). The boolean indicates whether the weave yielded (true) or completed (false):

: finite-generator ( Ctx Int -- | Yield Int )
  dup 0 i.<= if
    drop drop    # Done - just return, weave ends
  else
    tuck yield   # Yield current value
    rot 1 i.-    # Decrement
    finite-generator
  then
;

: main ( -- )
  [ finite-generator ] strand.weave
  3 strand.resume    # ( handle 3 true )
  drop drop          # ( handle )
  0 strand.resume    # ( handle 2 true )
  drop drop
  0 strand.resume    # ( handle 1 true )
  drop drop
  0 strand.resume    # ( handle 0 false ) - weave completed!
  if
    "More values" io.write-line
  else
    drop "Weave finished" io.write-line
  then
  drop  # drop handle
;

Resource Management

Weaves hold resources (channels internally). You must either:

  1. Resume to completion - Keep resuming until has_more is false
  2. Cancel explicitly - Call strand.weave-cancel to release resources
# BAD - resource leak!
[ infinite-generator ] strand.weave
10 strand.resume drop drop drop  # Weave abandoned, resources leak

# GOOD - explicit cancellation
[ infinite-generator ] strand.weave
10 strand.resume drop drop
strand.weave-cancel              # Clean up

The linter will warn about immediate drops of weave handles.

Type System Integration

The Yield effect appears in stack effect annotations:

: my-generator ( Ctx Int -- | Yield Int )
  #                      ^^^^^^^^^^^ Yield effect
  ...
;

This tells the type checker that the word participates in generator semantics. The effect propagates through callers.

Advanced: Structured Data

Weaves work well with union types for rich producer/consumer protocols:

union SensorReading {
  Reading { temp: Int, status: String }
}

: sensor-processor ( Ctx SensorReading -- | Yield SensorReading )
  # Transform the reading
  dup 0 variant.field-at    # Get temp
  classify-temp             # Classify it
  swap 1 variant.field-at   # Get status
  Make-Reading              # Create new reading

  swap yield                # Yield result, get next reading
  sensor-processor          # Process next
;

Comparison to Other Languages

FeatureSeq WeavesPython GeneratorsJavaScript Generators
BidirectionalYes (yield/resume)Yes (send())Yes (next(value))
ContextExplicit stack threadingImplicitImplicit
ConcurrencyBuilt on strands/channelsSingle-threadedSingle-threaded
Cancellationstrand.weave-cancelClose iteratorreturn()
Type systemYield effect trackedUntypedUntyped

Backpressure

Weaves provide backpressure through the pull model: a producer cannot advance past yield until the consumer explicitly calls strand.resume. The producer is suspended at yield, not running ahead buffering values — it is physically blocked waiting for a resume signal.

This means the consumer controls the production rate with no additional coordination needed:

# Infinite data source - but only computes on demand
: data-source ( Ctx Int -- | Yield Int )
  tuck yield        # Block here until consumer is ready
  rot 1 i.+
  data-source
;

: slow-consumer ( WeaveCtx -- )
  # Only pulls the next item when ready to process it
  0 strand.resume   # Producer runs exactly one step
  drop              # Process the value
  # ... do slow work ...
  slow-consumer
;

This is not buffered backpressure (as in Reactive Streams or Akka Streams, where a downstream signals demand counts). It is stricter: one resume produces exactly one yielded value. The producer cannot run ahead at all.

When to Use Weaves vs Strands

Use CaseMechanism
Independent concurrent tasksstrand.spawn
Producer/consumer with backpressureWeaves
Request/response patternsWeaves
Fire-and-forget parallelismstrand.spawn
Lazy sequencesWeaves
Stream transformationsWeaves

See Also