Types

Overview

rill is dynamically typed and type-safe. Types are checked at runtime, but type errors are always caught—there are no implicit conversions or coercions.

TypeSyntaxExample
String"text""hello"
Number123, 0.542, 0.9
Booltrue, falsetrue
List[a, b] or list[a, b]list["file.ts", 42]
Dict[k: v] or dict[k: v]dict[output: "text", code: 0]
Orderedordered[k: v]ordered[a: 1, b: "hello"]
Tupletuple[...] (positional)tuple[1, 2]
Datetimedatetime(...) or now()datetime("2024-01-15T10:30:00Z")
Durationduration(...)duration(...dict[days: 1, hours: 2])
Vectorhost-providedvector(voyage-3, 1024d)
Closure||{ }|x|($x * 2)
Typetype name or constructornumber, list(number), dict(a: number)

Key principles:

  • Type-safe: No implicit coercion—"5" + 1 errors, not "51" or 6
  • Type-locked variables: A variable that holds a string always holds a string
  • Value-based: All values are immutable, all comparisons by value
  • No null/undefined: Empty values are valid ("", [], [:]), but “no value” cannot exist
  • No truthiness: Conditions require actual booleans, not “truthy” values

The type keywords (string, number, bool, closure, list, dict, tuple, ordered, vector, datetime, duration, any, type) are reserved in the |...| closure position for anonymous typed parsing. See Closures for full documentation of anonymous typed closures.

Strings

Double-quoted text with variable interpolation using {$var}:

"hello world"
"Process {$filename} for review"
"Result: {$response}"

Escape sequences: \n, \t, \\, \", {{ (literal {), }} (literal })

Interpolation

Any valid expression works inside {...}:

"alice" => $name
3 => $a
5 => $b
true => $ok
"Hello, {$name}!"                    # Variable
"sum: {$a + $b}"                     # Arithmetic
"valid: {$a > 0}"                    # Comparison
"status: {$ok ? \"yes\" ! \"no\"}"   # Conditional
"upper: {$name -> .upper}"           # Method chain

Multiline Strings

Multiline strings use triple-quote syntax:

"World" => $name
"""
Hello, {$name}!
Line two
"""

Triple-quote strings support interpolation like regular strings.

See Strings for string methods.

Numbers

Used for arithmetic, exit codes, and loop limits:

42
0
3.14159
-7

Arithmetic operators: +, -, *, /, % (modulo)

Type constraint: All arithmetic operands must be numbers. No implicit conversion:

5 + 3                      # 8
# Error: Arithmetic requires number, got string
"5" + 1

Booleans

Literal true and false. Conditional expressions (?), loop conditions (@), and filter predicates require boolean values. Non-boolean values cause runtime errors.

true ? "yes" ! "no"        # "yes"
false ? "yes" ! "no"       # "no"

No truthiness: rill has no automatic boolean coercion. Empty strings, zero, and empty lists are not “falsy”—you must explicitly check:

"" -> .empty ? "empty" ! "has content"     # Use .empty method
0 -> ($ == 0) ? "zero" ! "nonzero"         # Use comparison

Type-Safe Negation

The negation operator (!) requires a boolean operand. There is no truthiness coercion:

!true                      # false
!false                     # true
"hello" -> .empty -> (!$)  # true (negates boolean from .empty)
!"hello"                   # ERROR: Negation requires boolean, got string
!0                         # ERROR: Negation requires boolean, got number

Use explicit boolean checks when needed:

"" -> .empty -> (!$) ? "has content" ! "empty"         # Negate boolean result
[1,2,3] -> .empty -> (!$) ? "has items" ! "none"   # Check non-empty

Lists

Ordered sequences of values. The bare [...] form and the keyword list[...] form are equivalent — list[...] is canonical (used in output and the LLM reference).

When list elements share a compound type but differ in sub-structure, rill infers the bare compound type. See Type Inference Cascade.

[1, 2, 3]         # bare form
list[1, 2, 3]     # keyword form (canonical)
[1, 2, 3] => $nums
$nums[0]                   # 1
$nums[-1]                  # 3 (last element)
$nums -> .len              # 3

List Spread

Inline elements from another list using ... spread syntax:

[1, 2] => $a
[...$a, 3]             # list[1, 2, 3]
[...$a, ...$a]         # list[1, 2, 1, 2] (concatenation)
[...[], 1]         # list[1] (empty spread contributes nothing)

Spread expressions evaluate before inlining:

[1, 2, 3] => $nums
[...($nums -> map {$ * 2})]  # list[2, 4, 6]

Spreading a non-list throws an error:

"hello" => $str
[...$str]              # Error: Spread in list literal requires list, got string

Access methods:

  • [0], [1] — Index access (0-based)
  • [-1], [-2] — Negative index (from end)
  • .head — First element (errors on empty)
  • .tail — Last element (errors on empty)
  • .at(n) — Element at index

Out-of-bounds access throws an error:

[] -> .at(0)           # Error: List index out of bounds
["a"] -> .at(5)        # Error: List index out of bounds

Use ?? for safe access with default:

["a"] => $list
$list[0] ?? "default"  # "a"

See Collections for iteration operators.

Dicts

Key-value mappings with identifier, number, boolean, variable, or computed keys. The bare [k: v] form and the keyword dict[...] form are equivalent — dict[...] is canonical.

[name: "alice", age: 30]         # bare form
dict[name: "alice", age: 30]     # keyword form (canonical)
[:]                               # empty dict (bare)
dict[]                            # empty dict (canonical)
# Identifier keys
[name: "alice", age: 30] => $person
$person.name               # "alice"
$person.age                # 30
# Number keys (including negative)
[1: "one", 2: "two", -1: "minus one"] => $numbers
1 -> $numbers              # "one"
(-1) -> $numbers           # "minus one"

# Boolean keys
[true: "yes", false: "no"] => $yesno
true -> $yesno             # "yes"

# Variable keys (key value from variable, must be string)
"status" => $key
[$key: "active"]       # dict[status: "active"]

# Computed keys (key from expression, must be string)
"user" => $prefix
[($prefix -> "{$}_name"): "alice"]  # dict[user_name: "alice"]

# Multi-key syntax (same value for multiple keys)
[["a", "b"]: 1]    # dict[a: 1, b: 1]
[[1, "1"]: "x"]    # dict[1: "x", "1": "x"] (mixed types)
[a: 0, ["b", "c"]: 1]  # dict[a: 0, b: 1, c: 1] (mixed entries)
[a: 0, ["a", "b"]: 1]  # dict[a: 1, b: 1] (last-write-wins)

# Multi-key dispatch
[["GET", "HEAD"]: "safe", list["POST", "PUT"]: "unsafe"] => $methods
"GET" -> $methods          # "safe"
"POST" -> $methods         # "unsafe"

Multi-key errors:

[[]: 1]            # Error: Multi-key dict entry requires non-empty list
[[list[1, 2], "a"]: 1]  # Error: Dict key must be string, number, or boolean, got list

Access patterns:

  • .field — Literal field access (identifier keys only)
  • .$key — Variable as key
  • .($i + 1) — Computed expression as key
  • .(a || b) — Alternatives (try keys left-to-right)
  • .field ?? default — Default value if missing
  • .?field — Existence check, literal key (returns bool)
  • .?$key — Existence check, variable key
  • .?($expr) — Existence check, computed key
  • .?field&type — Existence + type check (all forms support &type)

Note: Number and boolean keys require dispatch syntax (value -> dict) or bracket access. Dot notation (.1, .true) is not valid syntax.

Missing key access throws an error. Use ?? for safe access:

[:] => $d
$d.missing ?? ""           # "" (safe default)

Type-Aware Dispatch

Dict dispatch uses type-aware matching. Keys are matched by both value and type:

# Number vs string discrimination
[1: "number one", "1": "string one"] => $mixed
1 -> $mixed                # "number one" (number key)
"1" -> $mixed              # "string one" (string key)

# Boolean vs string discrimination
[true: "bool true", "true": "string true"] => $flags
true -> $flags             # "bool true" (boolean key)
"true" -> $flags           # "string true" (string key)

This enables pattern matching where the same semantic value (e.g., 1 vs "1") triggers different behavior based on type.

Dict Methods

MethodDescription
.keysAll keys as list
.valuesAll values as list
.entriesList of [key, value] pairs
[name: "test", count: 42] -> .keys      # ["count", "name"]
[name: "test", count: 42] -> .values    # [42, "test"]
[a: 1, b: 2] -> .entries                # [list["a", 1], list["b", 2]]

Reserved methods (keys, values, entries) cannot be used as dict keys.

Dict Closures

Closures in dicts have $ late-bound to the containing dict. See Closures for details.

[
  name: "toolkit",
  count: 3,
  str: ||"{$.name}: {$.count} items"
] => $obj

$obj.str    # "toolkit: 3 items" (auto-invoked)

Uniform Value Type

dict(T) asserts that every value in the dict matches type T. The dict itself is returned unchanged.

[a: 1, b: 2] -> :>dict(number)
# Result: dict[a: 1, b: 2]

An empty dict passes — no values to violate the constraint.

dict[] -> :>dict(number)
# Result: dict[]

Field Annotations

Dict type constructors support ^() inline field annotations. Annotations attach metadata to individual fields and appear on the type structure when you call .^type. See Closure Annotations for the full ^() syntax and TypeScript access patterns.

dict(^("A person's name") name: string, ^("Age in years") age: number)

Ordered

ordered is a first-class container produced by the ordered[...] literal syntax. It preserves key insertion order.

ordered[a: 1, b: "hello"] => $o
$o.^type.name
# Result: "ordered"

Use ordered for named argument unpacking:

|a, b| { "{$a}-{$b}" } => $fmt
ordered[a: 1, b: "hello"] -> $fmt(...)
# Result: "1-hello"

Key order in ordered is the insertion order. This differs from dict, which is unordered.

ordered converts to a plain object via toNative() — the NativeResult.value field holds { key: value, ... }.

Uniform Value Type

ordered(T) asserts that every entry value in the ordered container matches type T. The container is returned unchanged.

ordered[x: 1, y: 2] -> :>ordered(number)
# Result: ordered[x: 1, y: 2]

An empty ordered container passes — no values to violate the constraint.

Field Annotations

ordered type constructors support ^() inline field annotations. Annotations attach at each index in the type structure. See Closure Annotations for syntax and TypeScript access patterns.

ordered(^("X coordinate") x: number, ^("Y coordinate") y: number)

Tuples

Tuples are positional containers created with tuple[...] syntax.

Using Ordered for Named Unpacking

For named unpacking, use ordered[...]:

|a, b, c| { "{$a}-{$b}-{$c}" } => $fmt
dict[c: 3, a: 1, b: 2] -> $fmt(...)   # "1-2-3" (named, via dict; key order irrelevant)

Strict Validation

When invoking with ordered containers, missing required parameters error, and extra keys error:

|x, y|($x + $y) => $fn
ordered[x: 1, y: 2] -> $fn(...)        # 3

Parameter Defaults with Ordered

|x, y = 10, z = 20|($x + $y + $z) => $fn
ordered[x: 5] -> $fn(...)              # 35 (5 + 10 + 20)

Trailing Defaults with Tuples

Tuple type constructors accept default values on trailing positional fields. When you assert or check against a tuple type, values shorter than the full field count match if every omitted trailing field has a default.

Assign the type constructor to a variable, then use the variable in :? or : position:

tuple(string, number = 0) => $t
tuple["x"] -> :?$t
# Result: true

The value tuple["x"] has 1 element. The type has 2 fields, but the second field defaults to 0. The check passes because the omitted trailing field has a default.

tuple(string, number = 0) => $t
tuple["x"] -> :$t
# Result: tuple["x"]

The : assertion also accepts the shorter value. No field synthesis occurs — the returned value is unchanged. Use :> to fill missing fields with their defaults.

Defaults must appear at trailing positions only. A required field after a defaulted field is a type constructor error.

This matches the trailing-default behavior of dict and ordered type constructors.

Nested Default Synthesis

When a collection-typed field has no value and no explicit default, :> synthesizes it if all its children have defaults. The runtime seeds an empty collection and fills each child from the nested type.

dict[a: 1] -> :>dict(a: number, b: dict(c: number = 5))
# Result: dict[a: 1, b: dict[c: 5]]

When a field has an explicit collection default, :> hydrates that default through the nested type. Child defaults fill any fields the explicit default omits.

If any required child field lacks a default, the conversion raises RILL-R044.

Parallel Spread with Tuples

Use tuples with explicit spread ... to pass positional args in map:

|x, y|($x * $y) => $mul
[tuple[1, 2], tuple[3, 4]] -> map { $mul(...) }    # list[2, 12]

Uniform Value Type

tuple(T) asserts that every entry in the tuple matches type T. The tuple is returned unchanged.

tuple[1, 2, 3] -> :>tuple(number)
# Result: tuple[1, 2, 3]

An empty tuple passes — no values to violate the constraint.

Breaking change: The single-positional-argument form tuple(T) now defines a uniform value type, not a 1-element structural tuple. Use tuple(T1, T2) (two or more positional args) for structural tuples with specific element types.

Field Annotations

Tuple type constructors support ^() inline annotations on positional elements. Annotations attach at each index in the type structure. See Closure Annotations for syntax and TypeScript access patterns.

tuple(^("X coordinate") number, ^("Y coordinate") number)

Vectors

Vectors represent dense numeric embeddings from language models or other ML systems. Host applications provide vectors through embedding APIs.

Display format: vector(model, Nd) where model is the source model name and N is the dimension count.

app::embed("hello world") => $vec
$vec -> .model
# Result: "mock-embed"

Properties

PropertyTypeDescription
.dimensionsnumberNumber of dimensions in the vector
.modelstringSource model name
app::embed("hello world") => $vec
$vec -> .dimensions
# Result: 3

$vec -> .model
# Result: "mock-embed"

Methods

MethodReturnsDescription
.similarity(other)numberCosine similarity, range [-1, 1]
.dot(other)numberDot product
.distance(other)numberEuclidean distance, >= 0
.norm()numberL2 magnitude
.normalize()vectorUnit vector (preserves model)
app::embed("hello") => $a
app::embed("hi") => $b
$a -> .similarity($b)
# Result: 1.0

$a -> .dot($b)
# Result: 0.14

$a -> .distance($b)
# Result: 0.0

$a -> .norm
# Result: 0.37

$a -> .normalize -> .norm
# Result: 1.0

Comparison

Vectors support equality comparison (==, !=). Two vectors are equal when both model and all float elements match:

app::embed("test") => $v1
app::embed("test") => $v2
$v1 == $v2
# Result: true

Vectors from different models are never equal, even with identical data:

# Different models
app::embed("test", "model-a") => $v1
app::embed("test", "model-b") => $v2
$v1 == $v2
# Result: false

Behavioral Notes

  • Immutable: Vector data cannot be modified after creation
  • Always truthy: Vectors evaluate to true in boolean contexts (non-empty by construction)
  • No string coercion: Cannot be used in string interpolation or concatenation
  • No collection operations: Cannot use each, map, filter, fold on vectors
# Error: cannot coerce vector to string
"Result: {$vec}"

# Error: Collection operators require list, string, dict, or iterator, got vector
$vec -> each { $ * 2 }

Datetime

A datetime represents an instant in time stored as UTC milliseconds since the Unix epoch. It is an opaque scalar type — values are immutable and compared by their Unix timestamp.

Construction

Three forms construct a datetime value:

FormExampleNotes
ISO 8601 stringdatetime("2024-01-15T10:30:00Z")Accepts date-only and datetime with offset
Named componentsdatetime(...dict[year: 2024, month: 1, day: 15])UTC; hour, minute, second, ms default to 0
Unix millisecondsdatetime(...dict[unix: 1705312200000])UTC ms since epoch
datetime("2024-01-15T10:30:00Z") -> .iso()
# Result: "2024-01-15T10:30:00Z"

datetime(...dict[year: 2024, month: 1, day: 15]) -> .iso()
# Result: "2024-01-15T00:00:00Z"

datetime(...dict[unix: 0]) -> .iso()
# Result: "1970-01-01T00:00:00Z"

now()

now() returns the current UTC instant as a datetime.

now() -> .iso()

The test harness does not fix the clock, so # Result: is omitted. Pass nowMs in RuntimeContext to pin the instant in deterministic scripts.

Properties

UTC component properties decompose the stored timestamp:

PropertyTypeDescription
.yearnumberUTC year (e.g. 2024)
.monthnumberUTC month, 1–12
.daynumberUTC day, 1–31
.hournumberUTC hour, 0–23
.minutenumberUTC minute, 0–59
.secondnumberUTC second, 0–59
.msnumberUTC millisecond, 0–999
.unixnumberRaw UTC ms since epoch
.weekdaynumberISO weekday: 1 (Monday) – 7 (Sunday)
now() => $t
$t -> .year
$t -> .month
$t -> .weekday

String Output Methods

MethodReturnsDescription
.iso(offset?)stringFull ISO 8601 with timezone indicator (default UTC)
.date(offset?)stringYYYY-MM-DD portion
.time(offset?)stringHH:MM:SS portion

offset is hours east of UTC. Pass 2 for +02:00, -5 for -05:00, 5.5 for +05:30.

now() => $t
$t -> .iso(0)
$t -> .iso(2)
$t -> .date(0)
$t -> .time(0)

Local Properties

These properties apply the timezone offset from RuntimeContext automatically:

PropertyTypeDescription
.local_isostringISO 8601 at host timezone
.local_datestringYYYY-MM-DD at host timezone
.local_timestringHH:MM:SS at host timezone
.local_offsetnumberHost timezone offset in hours
now() => $t
$t -> .local_iso
$t -> .local_offset

Arithmetic

MethodArgumentReturnsDescription
.add(dur)durationdatetimeAdds duration to datetime; months applied first, then ms
.diff(other)datetimedurationAbsolute difference as a fixed-ms duration
now() => $t1
$t1 -> .diff($t1) => $gap
$gap -> .display
# Result: "0ms"
# Add one month (calendar duration)
now() -> .add(duration(...dict[months: 1])) -> .iso()
datetime("2024-03-01T00:00:00Z") -> .diff(datetime("2024-01-01T00:00:00Z")) -> .display
# Result: "60d"

Comparison

Datetimes support equality (==, !=) and ordering (<, >, <=, >=). Comparison uses the Unix timestamp directly.

now() == now()
# Result: true

now() <= now()
# Result: true

JSON

json() serializes a datetime as an ISO 8601 string with milliseconds. deserializeValue accepts an ISO 8601 string to reconstruct the datetime.

json(datetime("2024-01-15T10:30:00Z"))
# Result: "\"2024-01-15T10:30:00.000Z\""

String Interpolation

Interpolating a datetime produces its UTC ISO 8601 string (same as .iso()).

now() => $t
"Event at {$t}"

Empty Value

.empty returns datetime(unix: 0) — the Unix epoch.

now() -> .empty -> .iso()
# Result: "1970-01-01T00:00:00Z"

now() -> .empty -> .unix
# Result: 0

Behavioral Notes

  • Immutable: Datetime values cannot be modified after creation
  • Scalar: A single UTC timestamp; no timezone or locale stored on the value
  • String coercion permitted: Datetimes can appear in string interpolation; they format as ISO UTC
  • No collection operations: Cannot use each, map, filter, fold on datetimes

Extension Boundary

The core datetime type stores UTC timestamps and formats with numeric offsets only. IANA timezone names (e.g. "America/New_York") require the datetime-extension package, which is not part of core rill.

Leap-Second Note

rill uses POSIX time (Unix milliseconds). POSIX time does not model leap seconds. A timestamp represents continuous SI seconds since 1970-01-01T00:00:00Z with no leap-second gaps or repeats.


Duration

A duration represents a span of time. It stores two fields independently: months for calendar units and ms for fixed units. These fields never mix in arithmetic.

Construction

Two families of units construct duration values:

FormExampleNotes
Fixed unitsduration(...dict[days: 1, hours: 2])Collapses to ms; exact arithmetic
Calendar unitsduration(...dict[months: 3, years: 1])Collapses years to months; variable-length
Raw millisecondsduration(...dict[ms: 86400000])Direct ms count
duration(...dict[days: 1, hours: 2]) -> .display
# Result: "1d2h"

duration(...dict[months: 3]) -> .months
# Result: 3

duration(...dict[years: 1]) -> .months
# Result: 12

duration(...dict[ms: 5000]) -> .display
# Result: "5s"

Properties

Fixed-unit durations decompose their ms field using remainder arithmetic:

PropertyTypeDescription
.daysnumberfloor(ms / 86400000)
.hoursnumberRemainder hours after days
.minutesnumberRemainder minutes after hours
.secondsnumberRemainder seconds after minutes
.msnumberRemainder milliseconds after seconds
.monthsnumberCalendar months count
.total_msnumberRaw ms field; halts on calendar durations
duration(...dict[hours: 25]) -> .days
# Result: 1

duration(...dict[hours: 25]) -> .hours
# Result: 1

duration(...dict[hours: 25]) -> .total_ms
# Result: 90000000

Requesting .total_ms on a calendar duration halts execution:

# Error: total_ms is not defined for calendar durations
duration(...dict[months: 1]) -> .total_ms

Display

.display formats a duration as a compact string, omitting zero components. Zero duration displays as "0ms".

duration(...dict[days: 1, hours: 2, minutes: 30]) -> .display
# Result: "1d2h30m"

duration(...dict[years: 1, months: 3]) -> .display
# Result: "1y3mo"
now() => $t
$t -> .diff($t) -> .empty -> .display
# Result: "0ms"

Arithmetic

MethodArgumentReturnsDescription
.add(other)durationdurationSums months and ms fields independently
.subtract(other)durationdurationSubtracts fields; halts if result would be negative
.multiply(n)numberdurationMultiplies both fields by n; halts if n is negative
duration(...dict[hours: 1]) -> .add(duration(...dict[hours: 1])) -> .display
# Result: "2h"

duration(...dict[hours: 2]) -> .subtract(duration(...dict[hours: 1])) -> .display
# Result: "1h"

duration(...dict[hours: 1]) -> .multiply(3) -> .display
# Result: "3h"

Comparison

Equality compares both months and ms fields. Two durations are equal only when both fields match.

duration(...dict[hours: 48]) == duration(...dict[days: 2])
# Result: true

duration(...dict[months: 1]) == duration(...dict[months: 1])
# Result: true

Ordering compares the ms field only, and only when both durations have equal months fields. Comparing durations with different months halts:

duration(...dict[hours: 1]) < duration(...dict[hours: 2])
# Result: true

# Error: Cannot order durations with different calendar components
duration(...dict[months: 1]) < duration(...dict[hours: 24])

JSON

Fixed durations serialize as a number (raw ms). Calendar durations serialize as {"months": N, "ms": M}.

json(duration(...dict[hours: 1]))
# Result: "3600000"

json(duration(...dict[months: 1]))
# Result: "{\"months\":1,\"ms\":0}"

String Interpolation

Interpolating a duration produces its .display string.

"Gap: {duration(...dict[days: 3])}"
# Result: "Gap: 3d"
now() => $t
$t -> .diff($t) => $gap
"Gap: {$gap}"
# Result: "Gap: 0ms"

Empty Value

.empty returns duration(ms: 0).

now() => $t
$t -> .diff($t) -> .empty -> .display
# Result: "0ms"

Behavioral Notes

  • Immutable: Duration values cannot be modified after creation
  • Scalar: Stored as two independent fields; no normalization between fields
  • No negatives: Duration values are always non-negative; subtraction halts on negative results
  • String coercion permitted: Durations can appear in string interpolation; they format via .display

Extension Boundary

Core duration arithmetic is fixed-field only. Fractional months, business-day arithmetic, and calendar-aware duration normalization require the datetime-extension package.

Streams

A stream is a collection type that produces values over time. Unlike lists, streams emit chunks one at a time and carry a separate resolution value returned when the stream closes.

The type signature stream(T):R names both parts: T is the chunk type, and R is the resolution type.

stream(string):number   # chunks are strings, resolution is a number
stream(number)          # chunks are numbers, resolution is unconstrained
stream()                # unconstrained chunk and resolution types

Chunk Type

The chunk type T in stream(T):R constrains each emitted value. Inside a stream closure, yield emits the current pipe value as a chunk.

|x| {
  $x -> .upper -> yield
  $x -> .lower -> yield
} :stream(string)

yield appears as the terminator in a pipe chain. Use $x -> yield to emit a specific value, or bare yield to emit the current pipe value ($).

The yield keyword is only valid inside a closure annotated with :stream(T):R. Using yield without that annotation is a parse error.

Resolution Type

The resolution type R in stream(T):R constrains the value the stream returns when it closes. Call the stream variable as a function ($s()) to get the resolution value.

make_stream() => $s
$s()   # returns the resolution value

The resolution is produced by the stream’s final expression or an explicit return statement in the stream closure body. Host-provided streams supply the resolution from the underlying async producer.

A zero-chunk stream resolves immediately. Calling $s() returns the resolution value without consuming any chunks.

Single-Use Constraint

A stream can be iterated only once. Passing a stream to a collection operator (each, map, filter, fold) consumes all its chunks. After that, the stream is done and produces no further chunks.

make_stream() => $s
$s -> each { $ }   # consumes the stream
$s -> map { $ }    # Error: stream already consumed

Calling $s() after consuming the stream still returns the resolution value. The resolution is cached after the stream closes. $s() also works on any .next() step, including stale steps that can no longer advance. Only .next() fails on stale steps.

Stream as Collection

All four collection operators work on streams. They consume chunks as the stream emits them and collect results when the stream closes.

make_stream() => $s
$s -> map { $ * 2 }           # transforms each chunk, returns list
$s -> filter { $ > 0 }        # keeps matching chunks, returns list
$s -> fold(0) { $@ + $ }      # reduces all chunks to a single value

See Collections for full operator documentation including stream behavior.

For stream closure syntax and the :stream(T):R annotation on closures, see Closures.

See Also

DocumentDescription
Type SystemStructural types, type assertions, unions, type-locked variables
VariablesDeclaration, scope, $ binding
ClosuresClosure semantics and patterns
CollectionsList iteration operators
StringsString methods reference
ReferenceQuick reference tables