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).