StringStream - SML
Table of Contents
1. Introduction
For debugging purposes, it's useful to have a way to use a string as an input stream instead of a file.
It's also useful to have a "string writer" (analogous to Java's
StringStream
) for building up a string.
1.1. String reader
We would create a TextPrimIO.reader
object, something like:
fun mk_string_reader (source : string) : TextPrimIO.reader = let val pos = ref 0; fun read n = let val p = !pos; val m = min(n, size source - p); in pos := p + m; substring(source, p, m) end; val name = "<string \"" ^ (if size source <= 8 then source else (substring(source, 0, 8) ^"...")) ^ "\">"; in TextPrimIO.RD{name = name, chunkSize = size source, readVec = SOME(read), readArr = NONE, readVecNB = SOME(fn n => SOME(read n)), readArrNB = NONE, block = SOME(fn () => ()), canInput = SOME(fn () => true), avail = SOME(fn () => size source - !pos), getPos = SOME(fn () => !pos), setPos = SOME(fn k => if 0 <= k andalso k <= size source then pos := k else raise Fail "position out of bounds"), verifyPos = NONE, close = (fn () => ()), ioDesc = NONE} end;
This can be found in, e.g., section 8.3 of Andrew W Appel's Proposed interface for Standard ML Stream I/O (dated February 6, 1995).
If we wish to adhere to the Standard Basis documentation more faithfully, we should have a way to check if the stream was closed. Something like:
fun mk_string_reader (source : string) : TextPrimIO.reader = let val pos = ref 0; val is_closed = ref false; fun read n = if !is_closed then raise IO.ClosedStream else let val p = !pos; val m = min(n, size source - p); in pos := p + m; substring(source, p, m) end; val name = "<string \"" ^ (if size source <= 8 then source else (substring(source, 0, 8) ^"...")) ^ "\">"; in TextPrimIO.RD{name = name, chunkSize = size source, readVec = SOME(read), readArr = NONE, readVecNB = SOME(fn n => SOME(read n)), readArrNB = NONE, block = SOME(fn () => ()), canInput = SOME(fn () => !is_closed), avail = SOME(fn () => if !is_closed then raise IO.ClosedStream else size source - !pos), getPos = SOME(fn () => !pos), setPos = SOME(fn k => if !is_closed then raise IO.ClosedStream else if 0 <= k andalso k <= size source then pos := k else raise Fail "position out of bounds"), verifyPos = NONE, close = (fn () => is_closed := true), ioDesc = NONE} end;
We then have a "smart constructor" to make this an appropriate input stream type:
fun stringReader(source : string) = let val reader : TextIO.StreamIO.reader = mk_string_reader source; in TextIO.StreamIO.mkInstream(reader, "") end;
Ostensibly, we can use the result when trying to invoke
TextIO.StramIO.input1
.
1.2. String writer
I found myself looking for something analogous to Java's StringStream
,
when writing things out.
First, we need to construct a TextPrimIO.writer
for the
stream, which will just write to a string ref
buffer. This is the
lowest level to Standard ML's I/O model (hence the TextPrimIO
implements the PRIM_IO
signature).
We do not need to make the string buffer block upon writing to it, at
least I don't think so. A minimal implementation just requires the
writeVec
to write to a string buffer. For our purposes, a "string
buffer" is a reference to a string; "writing to it" amounts to updating
the reference by appending to it.
On SML/NJ, it seems that only writeVec'
is needed for an unbuffered
string stream. MLton also appears to only require writeVec'
. I should
really test this on other implementations…
(* Creates a string writer, which writes to the supplied string buffer. @param: buffer is the underlying string reference which accumulates everything the writer writes. @see: https://smlfamily.github.io/Basis/prim-io.html#SIG:PRIM_IO.writer:TY @see: https://smlfamily.github.io/Basis/mono-array-slice.html @see: https://smlfamily.github.io/Basis/mono-array.html *) fun stringWriter(buffer : string ref) : TextPrimIO.writer = let fun writeVec' (v : TextPrimIO.vector_slice) = (buffer := (!buffer)^(CharVectorSlice.vector v); (CharVectorSlice.length v)) fun writeArr' (arr : TextPrimIO.array_slice) = (buffer:=(!buffer)^(CharArraySlice.vector arr); (CharArraySlice.length arr)) fun writeVecNB' (v : TextPrimIO.vector_slice) = (buffer := (!buffer)^(CharVectorSlice.vector v); SOME (CharVectorSlice.length v)) fun writeArrNB' (arr : TextPrimIO.array_slice) = (buffer:=(!buffer)^(CharArraySlice.vector arr); SOME (CharArraySlice.length arr)) fun closing () = print ("Trying to close string stream?!?!?\n\n") in TextPrimIO.WR{ name = "<string>", chunkSize = 1, writeVec = SOME writeVec', writeArr = SOME writeArr', writeVecNB = SOME writeVecNB', writeArrNB = SOME writeArrNB', block = NONE, canOutput = NONE, getPos = NONE, setPos = NONE, endPos = NONE, verifyPos = NONE, close = closing, ioDesc = NONE} end;
Now, the Standard ML I/O model has a stream "facade" (if I may
borrow such an anachronistic term) wrapping around a
"primitive" writer
object. We use an outstream
when invoking
TextIO.output
, in order to write to the stream. We will have
IO.NO_BUF
mode for our string output stream, meaning we write directly
to the writer without buffering (thus no need for "flushing" the
stream).
We have a smart constructor for a "string stream" from a "string writer":
fun stringStream(buffer : string ref) = let val writer : TextIO.StreamIO.writer = stringWriter(buffer) in TextIO.StreamIO.mkOutstream(writer, IO.NO_BUF) end;
Now we can show an example usage:
val b1 : string ref = ref ""; val ss1 = stringStream(b1); fun ss_print (s : string) = let val cv : CharVector.vector = s in TextIO.StreamIO.output(ss1, cv) end; ss_print("foobar"); print("\n\n!b1 = "^(!b1)^"\n\n"); (* then `!b1` evaluates to "foobar" *)
Another approach to constructing a string stream would be to have its
buffer be a reference to a list of strings, but this would be mildly
inefficient since it'd be a stack (and thus a reverse
would be needed
to make the order correct).