Intermediate

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
"printNl shows an object as code; displayNl shows it for humans."
'Smalltalk' printNl.
'Smalltalk' displayNl.
42 printNl.
3.5 displayNl.
#(1 2 3) printNl.

"Transcript accepts non-string objects via display: and print:."
Transcript display: 42; cr.
Transcript print: 'quoted'; cr.

"Building a line from pieces with , and printString."
| language year |
language := 'Smalltalk'.
year := 1980.
Transcript showCr: language , ' appeared in ' , year printString , '.'.

"Template substitution: %1, %2 are replaced by the arguments."
Transcript showCr: ('%1 is %2 years old.' bindWith: language with: 2026 - year).

"Numbers can print themselves in any base."
Transcript showCr: (255 printString: 16)

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:

1
2
3
4
5
6
7
8
9
| name a b |
Transcript showCr: 'What is your name?'.
name := stdin nextLine.
Transcript showCr: 'Hello, ' , name , '!'.

Transcript showCr: 'Enter two numbers:'.
a := stdin nextLine asNumber.
b := stdin nextLine asNumber.
Transcript show: 'Their sum is '; showCr: (a + b) printString

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
| out log in lineNumber |

"Open for writing: creates the file, or overwrites it if present."
out := FileStream open: 'quotes.txt' mode: FileStream write.
out nextPutAll: 'The best way to predict the future'; nl.
out nextPutAll: 'is to invent it.'; nl.
out close.

"Open for appending: new output lands at the end of the file."
log := FileStream open: 'quotes.txt' mode: FileStream append.
log nextPutAll: '-- Alan Kay'; nl.
log close.

"Open for reading and process the file line by line."
in := FileStream open: 'quotes.txt' mode: FileStream read.
lineNumber := 1.
[ in atEnd ] whileFalse: [
    Transcript show: lineNumber printString; show: ': '; showCr: in nextLine.
    lineNumber := lineNumber + 1 ].
in close

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
| f out |

"ifFail: supplies a fallback value instead of halting the program."
f := FileStream open: 'missing.txt' mode: FileStream read ifFail: [ nil ].
f isNil
    ifTrue: [ Transcript showCr: 'Could not open missing.txt' ]
    ifFalse: [ Transcript showCr: f nextLine. f close ].

"ensure: guarantees cleanup - the close runs no matter what."
out := FileStream open: 'notes.txt' mode: FileStream write.
[ out nextPutAll: 'written safely'; nl ] ensure: [ out close ].
Transcript showCr: 'File written and closed'

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Pull the GNU Smalltalk image
docker pull sl4m/gnu-smalltalk:latest

# Run the output example
docker run --rm -v $(pwd):/app -w /app sl4m/gnu-smalltalk gst output.st

# The input example reads stdin, so add -i and pipe the answers in
printf 'Ada\n7\n35\n' | docker run --rm -i -v $(pwd):/app -w /app sl4m/gnu-smalltalk gst read_input.st

# Run the file I/O example (quotes.txt is created in your current directory)
docker run --rm -v $(pwd):/app -w /app sl4m/gnu-smalltalk gst file_io.st

# Run the error-handling example
docker run --rm -v $(pwd):/app -w /app sl4m/gnu-smalltalk gst safe_read.st

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 streamnextPutAll:, nl, nextLine, atEnd, and close work identically on Transcript-style output, stdin, and FileStream instances. Code written against the protocol does not care where the bytes go.
  • printNl vs displayNl — Every object has two self-descriptions: printString (code-like, strings keep quotes) and displayString (human-readable, quotes stripped). Choose displayNl for user-facing output.
  • Transcript accepts any objectshow: and showCr: expect Strings, but display: and print: take any object and render it, so you rarely need manual conversion just to print.
  • Formatting is messages, not format stringsbindWith:with: fills %1-style templates, and messages like printString: 16 handle radix conversion. There is no separate formatting language to learn.
  • Reading input is explicit and two-stepstdin nextLine always answers a String; converting it is a separate message (asNumber), which answers nil for bad input instead of failing silently.
  • File modes come from the classFileStream read, FileStream write, and FileStream append are messages to FileStream itself, passed to open:mode:. Writing truncates; appending preserves.
  • Failure handling is just blocksopen:mode:ifFail: evaluates your fallback block when the open fails, and ensure: guarantees cleanup like a finally clause — both built from ordinary blocks rather than special syntax.

Running Today

All examples can be run using Docker:

docker pull sl4m/gnu-smalltalk:latest
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining