Most I/O code only operates on one stream at a time. The
input-stream and
output-stream variables are implicit parameters used by many I/O words. Using this idiom improves code in three ways:
| • | Code becomes simpler because there is no need to keep a stream around on the stack. |
| • | Code becomes more robust because with-input-stream and with-output-stream automatically close the streams if there is an error. |
| • | Code becomes more reusable because it can be written to not worry about which stream is being used, and instead the caller can use with-input-stream or with-output-stream to specify the source or destination for I/O operations. |
For example, here is a program which reads the first line of a file, converts it to an integer, then reads that many characters, and splits them into groups of 16:
USING: continuations kernel io io.files math.parser splitting ;
"data.txt" utf8 <file-reader>
dup stream-readln string>number over stream-read 16 group
swap dispose
This code has two problems: it has some unnecessary stack shuffling, and if either
stream-readln or
stream-read throws an I/O error, the stream is not closed because
dispose is never reached. So we can add a call to
with-disposal to ensure the stream is always closed:
USING: continuations kernel io io.files math.parser splitting ;
"data.txt" utf8 <file-reader> [
dup stream-readln string>number over stream-read
16 group
] with-disposal
This code is robust, however it is more complex than it needs to be. This is where the default stream words come in; using them, the above can be rewritten as follows:
USING: continuations kernel io io.files math.parser splitting ;
"data.txt" utf8 <file-reader> [
readln string>number read 16 group
] with-input-stream
An even better implementation that takes advantage of a utility word:
USING: continuations kernel io io.files math.parser splitting ;
"data.txt" utf8 [
readln string>number read 16 group
] with-file-reader