I/O Operations in Standard ML
Learn console input, formatted output, file reading and writing, and I/O error handling in Standard ML using the TextIO structure with Docker-ready examples
Input and output are where Standard ML’s functional purity meets the messy real world. Unlike Haskell, which quarantines side effects inside the IO monad, SML takes a pragmatic approach: I/O operations are ordinary functions that perform side effects directly. Because SML uses strict (eager) evaluation, those effects happen in a predictable order — you can read a line, then write a file, then print a message, and they occur exactly in that sequence.
Almost all text I/O in SML lives in the Basis Library’s TextIO structure. It provides the standard streams (TextIO.stdIn, TextIO.stdOut, TextIO.stdErr), file operations (openIn, openOut, openAppend), and reading functions that return option values instead of failing — a very ML way to handle end-of-file. Where a C programmer checks for EOF and a Python programmer catches StopIteration, an SML programmer pattern matches on SOME line versus NONE.
In this tutorial you’ll build formatted console output with Real.fmt and StringCvt padding, read standard input with a recursive loop, write and read files, and handle missing-file errors with the IO.Io exception. Everything runs in the SML/NJ Docker image — no local installation needed.
Formatted Console Output
The print function only accepts strings, so producing formatted output is a matter of converting values to strings and combining them. The Basis Library provides Int.toString, Real.toString, Real.fmt for precision control, and StringCvt.padLeft/padRight for column alignment.
Create a file named formatted_output.sml:
| |
Key points:
Real.fmt (StringCvt.FIX (SOME 2))formats a real with a fixed number of decimal places.StringCvt.SCIgives scientific notation andStringCvt.GENpicks the shorter of the two.StringCvt.padLeft #" " 6 spads stringswith spaces on the left to width 6 — right-aligning it.padRightleft-aligns. The#" "is SML’s character literal syntax for a space.- There is no
printfin the Basis Library; idiomatic SML composes small conversion functions with^instead of interpreting a format string at runtime.
Reading Standard Input
TextIO.inputLine TextIO.stdIn reads one line and returns SOME line (newline included) or NONE at end of input. Because the result is an option, the type system forces you to decide what happens at end-of-file — there is no way to forget the EOF case. As a functional language, SML expresses the read loop as a recursive function rather than a while loop.
Create a file named read_input.sml:
| |
The case expression pattern matches on the option result: NONE ends the recursion with a summary, and SOME line uppercases the line with String.map Char.toUpper and recurses. This tail-recursive loop is the standard SML replacement for an imperative while (fgets(...)) pattern.
File Reading and Writing
File I/O follows the same open/operate/close shape found in most languages, but reading functions still return option values, and failures raise the IO.Io exception, which carries the file name and the failing operation in a record.
Create a file named file_io.sml:
| |
Notice the two reading styles: TextIO.inputAll slurps the entire remaining stream into one string (convenient for small files), while the recursive numberLines function processes one line at a time (better for large files or streaming transformations). The IO.Io exception is a record {name, function, cause} — pattern matching with {name, ...} extracts just the file name and ignores the rest.
Running with Docker
| |
SML/NJ prints its version banner and val bindings for each top-level declaration before and alongside your program’s output; the sections below show only what the programs themselves print.
Expected Output
Running formatted_output.sml:
Language: Standard ML
First appeared: 1983
Pi (default): 3.14159
Pi (2 places): 3.14
item | count
bindings | 4
years | 43
Running read_input.sml with the piped input shown above:
Line 1: HELLO
Line 2: FUNCTIONAL WORLD
Done. Read 2 line(s).
Running file_io.sml:
--- File contents ---
Standard ML
OCaml
Haskell
--- Numbered ---
1: Standard ML
2: OCaml
3: Haskell
Could not open: missing.txt
Streams, Flushing, and stderr
TextIO exposes three standard streams:
TextIO.stdIn : TextIO.instream
TextIO.stdOut : TextIO.outstream
TextIO.stdErr : TextIO.outstream
print s is shorthand for writing to standard output, but writing to standard error requires the explicit form:
TextIO.output (TextIO.stdErr, "warning: something odd\n")
Output streams are buffered, so a prompt without a trailing newline may not appear before a read. Call TextIO.flushOut TextIO.stdOut after printing a prompt to force it out — a common pattern when writing interactive programs:
fun prompt msg =
( print msg
; TextIO.flushOut TextIO.stdOut
; TextIO.inputLine TextIO.stdIn )
Closing an output stream with TextIO.closeOut flushes it automatically, which is why file_io.sml needs no explicit flush.
Key Concepts
TextIOis the hub for text I/O — standard streams (stdIn,stdOut,stdErr), file operations (openIn,openOut,openAppend), and reads/writes all live in this one Basis Library structure.- End-of-file is an
option, not an error —TextIO.inputLinereturnsSOME lineorNONE, so the type system forces you to handle EOF explicitly with pattern matching. - Read loops are recursive functions — idiomatic SML replaces
while-style input loops with tail-recursive functions that pattern match on each read result. inputLinekeeps the trailing newline — strip it yourself (e.g., withString.isSuffixandString.substring) when you need the bare line.- There is no
printf— build formatted output by composingInt.toString,Real.fmt, andStringCvt.padLeft/padRightwith the^concatenation operator. - I/O failures raise
IO.Io— the exception carries a{name, function, cause}record, so ahandle IO.Io {name, ...}clause can report exactly which file failed. - Choose
inputAllor line-at-a-time deliberately —inputAllis convenient for small files; recursiveinputLineloops scale to large or streaming input. - Buffered output needs flushing before interactive reads — use
TextIO.flushOutafter a prompt, or rely oncloseOut, which flushes on close.
Running Today
All examples can be run using Docker:
docker pull eldesh/smlnj:latest
Comments
Loading comments...
Leave a Comment