I/O Operations in Smalltalk
Learn console input, formatted output, and file reading and writing in Smalltalk - all through message passing to stream objects, with Docker-ready GNU Smalltalk examples
In most languages, input and output go through special built-in functions: printf, scanf, open, read. Smalltalk has none of these. In a language where everything is an object and every action is a message, I/O is simply message passing to stream objects. The console is a stream. A file is a stream. Standard input is a stream. Once you learn the stream protocol — nextPutAll: to write, nextLine to read, atEnd to test for exhaustion — you can talk to any of them the same way.
You have already met the output side in Hello World: Transcript is the global object representing program output, and show: and cr are messages it responds to. This page goes further. You will see the difference between an object’s machine representation and its human representation (printNl versus displayNl), build formatted output from templates, read and parse input typed on standard input, write and append to files with FileStream, read them back line by line, and handle a missing file without crashing.
GNU Smalltalk, the implementation in our Docker image, is unusually well suited to this topic. Unlike the image-based environments (Pharo, Squeak) where I/O flows through graphical tools, GNU Smalltalk behaves like a traditional Unix scripting language: it reads standard input, writes standard output, and works comfortably in pipelines — while remaining pure Smalltalk underneath.
Console Output Beyond Hello World
Every object in Smalltalk can render itself two ways. printString answers a representation you could paste back into code — strings keep their quotes, characters keep their $ prefix. displayString answers a representation meant for humans — strings appear without quotes. The convenience messages printNl and displayNl print each representation followed by a newline, and Transcript accepts any object at all through display: and print: (whereas show: expects a string).
Create a file named output.st:
| |
Three things are worth noticing. showCr: is a small convenience over the show: / cr cascade from Hello World — it prints the string and the newline in one message. bindWith:with: is Smalltalk’s template formatting: the receiver is an ordinary string containing %1 and %2 placeholders, filled in with the display representation of each argument. And printString: 16 shows that even radix formatting is just a message to the number itself — there is no format-string mini-language to memorize, only objects that know how to describe themselves.
Reading from Standard Input
GNU Smalltalk provides the global stdin, a FileStream connected to standard input. Sending it nextLine answers one line of input as a String, without the trailing newline. Since the result is an ordinary String object, parsing is just more message passing: asNumber converts the text to a number (answering nil if the text is not numeric, rather than silently producing garbage).
Create a file named read_input.st:
| |
There is no special “input statement” here. stdin is a stream object like any other, nextLine is the same message you will use to read files below, and conversion is an explicit message to the string — consistent with Smalltalk’s rule that nothing is coerced silently.
Writing and Reading Files
Files are handled by the FileStream class. You open one by sending open:mode: to the class, where the mode comes from FileStream itself: FileStream read, FileStream write (create or overwrite), or FileStream append (add to the end). What comes back is a stream that speaks the same protocol as Transcript and stdin: nextPutAll: writes a string, nl writes a newline, nextLine reads a line, atEnd tests whether anything is left. When you are done, close releases the file.
Create a file named file_io.st:
| |
The reading loop is pure Smalltalk control flow: [ in atEnd ] whileFalse: [ ... ] sends whileFalse: to a block, exactly as you saw in the control flow tutorial — no special end-of-file sentinel value to compare against. If you want the entire file as one String instead of iterating, send the stream contents.
Notice how little is file-specific here. nextPutAll: and nl wrote to a file exactly as they would write to any stream, and nextLine read from the file exactly as it read from stdin a moment ago. Learning one stream is learning them all.
Handling I/O Failures
Opening a file that does not exist is the most common I/O failure, and GNU Smalltalk offers a graceful idiom for it: open:mode:ifFail: takes a block that is evaluated — and whose value is answered — when the open fails, so you can substitute nil (or a default stream) instead of halting the program. For cleanup, the ensure: message on a block guarantees that a second block runs whether the first completes normally or not — Smalltalk’s equivalent of finally, built from nothing but blocks and messages.
Create a file named safe_read.st:
| |
Both idioms are ordinary message sends. ifFail: hands control to a block you supply; ensure: is a message understood by blocks themselves. Smalltalk needs no dedicated error-handling syntax for I/O because blocks — deferred, first-class pieces of code — already express “what to do instead” and “what to do afterward.”
Running with Docker
| |
Note the -i flag on the second command: it keeps the container’s standard input open so the piped text reaches stdin inside the script. Because the examples mount your current directory as /app, the files created by file_io.st and safe_read.st appear right next to your scripts.
Expected Output
output.st:
'Smalltalk'
Smalltalk
42
3.5
(1 2 3 )
42
'quoted'
Smalltalk appeared in 1980.
Smalltalk is 46 years old.
FF
read_input.st (with Ada, 7, and 35 piped in as shown above):
What is your name?
Hello, Ada!
Enter two numbers:
Their sum is 42
file_io.st:
1: The best way to predict the future
2: is to invent it.
3: -- Alan Kay
safe_read.st:
Could not open missing.txt
File written and closed
Key Concepts
- All I/O is message passing to streams — There are no I/O keywords or built-in functions. The console, standard input, and files are all stream objects, and reading or writing means sending them messages.
- One protocol covers every stream —
nextPutAll:,nl,nextLine,atEnd, andclosework identically onTranscript-style output,stdin, andFileStreaminstances. Code written against the protocol does not care where the bytes go. printNlvsdisplayNl— Every object has two self-descriptions:printString(code-like, strings keep quotes) anddisplayString(human-readable, quotes stripped). ChoosedisplayNlfor user-facing output.Transcriptaccepts any object —show:andshowCr:expect Strings, butdisplay:andprint:take any object and render it, so you rarely need manual conversion just to print.- Formatting is messages, not format strings —
bindWith:with:fills%1-style templates, and messages likeprintString: 16handle radix conversion. There is no separate formatting language to learn. - Reading input is explicit and two-step —
stdin nextLinealways answers a String; converting it is a separate message (asNumber), which answersnilfor bad input instead of failing silently. - File modes come from the class —
FileStream read,FileStream write, andFileStream appendare messages toFileStreamitself, passed toopen:mode:. Writing truncates; appending preserves. - Failure handling is just blocks —
open:mode:ifFail:evaluates your fallback block when the open fails, andensure:guarantees cleanup like afinallyclause — both built from ordinary blocks rather than special syntax.
Running Today
All examples can be run using Docker:
docker pull sl4m/gnu-smalltalk:latest
Comments
Loading comments...
Leave a Comment