Streams

Streams provide lazy evaluation for processing sequences. Unlike arrays, which compute everything up front, streams only produce values when you ask for them — making them ideal for large datasets, pipelines, and even infinite sequences.

Creating streams

From a starting value

Stream from: creates an infinite stream of consecutive integers:

s := Stream from: 1  // => _
s take: 5             // => [1,2,3,4,5]

With a custom step

Stream from:by: lets you control how each value is produced from the last:

evens := Stream from: 0 by: [:n | n + 2]  // => _
evens take: 5                               // => [0,2,4,6,8]
powers := Stream from: 1 by: [:n | n * 2]  // => _
powers take: 6                               // => [1,2,4,8,16,32]

From a collection

Stream on: wraps an existing collection as a stream:

s := Stream on: #(10, 20, 30)  // => _
s asList                        // => [10,20,30]

Collections also respond to stream:

s := #(1, 2, 3) stream  // => _
s class                  // => Stream
s asList                 // => [1,2,3]

Lazy pipeline operations

Stream operations are lazy — they return a new stream without evaluating anything. Values are only computed when a terminal operation forces them.

Transforming with collect:

squares := (Stream from: 1) collect: [:n | n * n]  // => _
squares take: 5                                      // => [1,4,9,16,25]

Filtering with select: and reject:

odds := (Stream from: 1) select: [:n | n isOdd]  // => _
odds take: 5                                       // => [1,3,5,7,9]
noThrees := (Stream from: 1) reject: [:n | (n % 3) =:= 0]  // => _
noThrees take: 5                                              // => [1,2,4,5,7]

Skipping with drop:

s := (Stream from: 1) drop: 3  // => _
s take: 4                       // => [4,5,6,7]

Chaining operations

Pipeline operations compose naturally — each returns a new stream:

result := ((Stream from: 1) select: [:n | n isEven]) collect: [:n | n * n]  // => _
result take: 4  // => [4,16,36,64]

Terminal operations

Terminal operations force evaluation and return a concrete value.

take: — materialize the first N elements

take: returns a List:

(Stream from: 10) take: 3  // => [10,11,12]

asList — materialize all elements

Only use asList on finite streams (from collections):

(Stream on: #(1, 2, 3)) asList  // => [1,2,3]

inject:into: — fold/reduce

sum := (Stream on: #(1, 2, 3, 4, 5)) inject: 0 into: [:acc :n | acc + n]  // => _
sum  // => 15

detect: — find the first match

(Stream from: 1) detect: [:n | n > 10]  // => 11
(Stream on: #(1, 2, 3)) detect: [:n | n > 100]  // => nil

anySatisfy: and allSatisfy:

(Stream on: #(1, 2, 3, 4)) anySatisfy: [:n | n > 3]  // => true
(Stream on: #(1, 2, 3, 4)) allSatisfy: [:n | n > 0]  // => true
(Stream on: #(1, 2, 3, 4)) allSatisfy: [:n | n > 2]  // => false

do: — iterate with side effects

count := 0                                              // => _
(Stream on: #(1, 2, 3)) do: [:n | count := count + n]  // => _
count                                                    // => 6

Infinite streams

Because streams are lazy, you can work with infinite sequences safely. Just make sure to limit output with take: or detect::

fibs := Stream from: #(1, 1) by: [:pair | #(pair at: 2, (pair at: 1) + (pair at: 2))]  // => _
first10 := (fibs take: 10) collect: [:pair | pair at: 1]  // => _
first10  // => [1,1,2,3,5,8,13,21,34,55]

Collection interop

Streams integrate with Beamtalk's collection protocol. Convert any collection to a stream and back:

original := #(10, 20, 30, 40, 50)                        // => _
filtered := (original stream select: [:n | n > 20]) asList  // => _
filtered  // => [30,40,50]
"hello" stream class  // => Stream

Empty streams

All operations handle empty streams gracefully:

empty := Stream on: #()                     // => _
empty asList                                 // => []
(empty select: [:n | true]) asList           // => []
(empty collect: [:n | n * 2]) asList         // => []
empty detect: [:n | true]                    // => nil
empty anySatisfy: [:n | true]                // => false
empty allSatisfy: [:n | true]                // => true

Summary

Construction:

Stream from: start                 → infinite stream (start, start+1, ...)
Stream from: start by: [:n | ...]  → infinite stream with custom step
Stream on: collection              → finite stream from collection
collection stream                  → finite stream (List, String, Set)

Lazy pipeline (return a new Stream):

stream collect: [:n | ...]   → transform each element
stream select: [:n | ...]    → keep matching elements
stream reject: [:n | ...]    → remove matching elements
stream drop: n               → skip first n elements

Terminal operations (force evaluation):

stream take: n                          → List of first n elements
stream asList                           → List of all elements
stream inject: init into: [:acc :n | …] → folded value
stream detect: [:n | ...]               → first match or nil
stream anySatisfy: [:n | ...]           → Boolean
stream allSatisfy: [:n | ...]           → Boolean
stream do: [:n | ...]                   → nil (side effects)

Exercises

1. First 10 squares. Use a stream to generate the first 10 perfect squares (1, 4, 9, 16, ...).

Hint
squares := (Stream from: 1) collect: [:n | n * n]
squares take: 10    // => [1,4,9,16,25,36,49,64,81,100]

2. Find a number. Using an infinite stream starting from 1, find the first number greater than 100 that is divisible by 7.

Hint
(Stream from: 1) detect: [:n | n > 100 and: [(n % 7) =:= 0]]
// => 105

detect: stops at the first match — safe on infinite streams.

3. Powers of 2. Create an infinite stream of powers of 2 (1, 2, 4, 8, 16, ...) using from:by: and take the first 8 elements.

Hint
powers := Stream from: 1 by: [:n | n * 2]
powers take: 8    // => [1,2,4,8,16,32,64,128]

The step block [:n | n * 2] produces each value by doubling the previous one.

Next: Chapter 24 — Reflection & Metaprogramming