DocsStandard library

Streams and iterators.

Traverse a sequence one step at a time with explicit state.

A stream is useful when elements can be produced or processed a little at a time. In Fango a stream is simply an initial state plus a step function. There’s no suspended producer sitting on a stack, which keeps them easy to reason about.

API reference: Stream · Iterator

Transform a stream.

import Stream

main() =
    values = Stream.fromList [1, 2, 3, 4]
    doubled = Stream.map { value -> value * 2 } values
    print (Stream.toList (Stream.take 2 doubled))

This prints [2, 4]. Building a stream and transforming it doesn’t run any of your callbacks. Work only happens when a traversal such as toList, fold, or forEach calls the step function as needed.

Define a producer.

Stream.unfold initial step describes a sequence in terms of its progress. Return Nothing to stop, or Just (element, nextState) to produce an element:

import Stream

step : Int -> Maybe (Int, Int)
step number = if number > 4 then Nothing else Just (number, number + 1)

main() =
    numbers = Stream.unfold 1 step
    print (Stream.toList numbers)
    print (Stream.fold (+) 0 numbers)
[1, 2, 3, 4]
10

The state starts at 1, and each step yields that number and moves on to the next. Each of the two traversals starts again from the stored initial state, and the fold doesn’t need an intermediate list.

A producer’s state can just as well be the remaining input, or a record of parsing progress. take adds a count, and a count that isn’t positive never steps the input. filter may need several input steps to produce one output. zip steps the left side before the right, so it can consume a left element that has no partner when the right side ends first.

Traverse and repeat.

fold combine initial stream passes the element first and the accumulator second. forEach does some work for each element without collecting anything.

Every new traversal starts from the stored initial state, but any effects still happen each time. A stream that advances a mutable reader shares that reader’s position, so starting the traversal again doesn’t rewind the input.

Advance an iterator.

An iterator packages a current state with a step function. Unlike a mutable cursor, advancing it gives you a new iterator:

import Iterator

step : Int -> Maybe (Int, Int)
step number = if number > 3 then Nothing else Just (number, number + 1)

main() =
    first = Iterator.start 1 step
    case Iterator.next first of
        Nothing -> print "finished"
        Just (number, rest) ->
            print number
            case Iterator.next rest of
                Nothing -> print "finished"
                Just (nextNumber, _) -> print nextNumber
    case Iterator.next first of
        Nothing -> print "finished"
        Just (number, _) -> print number
1
2
1

To move forward, use rest. The original first still holds the original state, so stepping it again gives 1 again. If the step performs effects, they repeat too, because immutability can’t undo them.

When you want this kind of control over a stream’s traversal, Stream.withCursor hands an iterator to a callback.

Keep resource work inside its scope.

Open a file or socket around the traversal that consumes it. Stopping early doesn’t run a hidden finalizer for the producer. A stream that carries a scoped reader permission can’t escape the reader’s callback, even inside another container. Resources and scopes explains both this static permission rule and external cleanup.