Finite or live¶
A file is replayable, so reading it gives a Seq: the documents end, and
every step in the book applies. Standard input arrives once, as it is
written, so reading it gives a Stream. hql check prints each type:
source(file("examples/io/sightings.jsonl"))
Seq<Data>
source(stdin, Json)
Stream<Data>
That is the difference the streams chapter drew. map,
filter and take run over standard input one document at a time, as it
arrives. A step that needs the whole input is a type error until the stream is
bounded with take(n) or batched with collect(n):
source(stdin, Json) | size
check error[type-mismatch]: `size` needs a finite input, and Stream<Data> may not end; bound it first with `take(n)`, or batch it with `collect(n)`
--> <expression>:1:23
|
1 | source(stdin, Json) | size
| ^^^^
The same program checks over a file and over a pipe, and the checker reports which steps each one allows.
One document at a time with tap¶
tap(endpoint, Format) reads any readable endpoint as a Stream, one
document at a time, holding only the current one in memory. Over a file,
source holds every document and gives a Seq; tap holds one and gives a
Stream, so a file larger than memory is read with tap:
tap(file("examples/io/sightings.jsonl"))
Stream<Data>
tap(file("examples/io/sightings.jsonl")) | take(2) | map(s => s.species)
["barn owl", "wren"]
Over a live endpoint the two give the same stream: tap(stdin, Json) is
source(stdin, Json). Over a file's end, tap gives each document appended
after the tap began, as tail -F does, and waits for a file that does not
exist yet:
tap(file("app.log").end, Lines) | take(1)
waits for the next line written to app.log, and gives it.
A format that is one whole document, such as Text, is not read one at a
time:
tap(file("examples/io/counts.csv"), Text)
check error[type-mismatch]: Text is one document, so it is not read one at a time; read it whole with `source`
--> <expression>:1:5
|
1 | tap(file("examples/io/counts.csv"), Text)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Batches¶
collect(n) gathers consecutive values into lists of n, and a list is
finite, so a stream is answered in pieces:
tap(file("examples/io/sightings.jsonl")) | collect(2) | map(b => b | size) | take(2)
[2, 2]
Written by sink, each batch is one document:
tap(file("examples/io/sightings.jsonl")) | map(s => s.species) | collect(2) | sink(stdout, Json)
["barn owl","wren"]
["barn owl","kingfisher"]
Over standard input the same program writes each array as soon as its second value has arrived:
$ cat examples/io/sightings.jsonl | hql 'source(stdin, Json) | map(s => s.species) | collect(2) | sink(stdout, Json)'
["barn owl","wren"]
["barn owl","kingfisher"]
What each shape allows¶
A Seq<T> is a Stream<T> that ends, so every rule for a stream holds for a
sequence, and a sequence has more:
Seq<T> |
Stream<T> that is not a Seq |
|
|---|---|---|
source gives it |
over a Replayable endpoint or a channel |
over a live endpoint |
tap |
reads it one value at a time | reads it as it is |
target |
writes one document | a type error |
sink |
writes one document per element | writes one document per value |
the result, by --to |
one whole text | one piece per value, as it arrives |
size, sort, xs[i], contains |
accepted | a type error |
filter, map, take, drop, tee |
accepted | accepted, one value at a time |
collect(n) |
gives List<List<T>> |
gives Stream<List<T>>, in batches |
emit keeps its meaning from the streams chapter: it gives the members of
each list a stream carries. It is not needed to turn a Seq into a Stream,
because a Seq already is one.