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.
