Intermediate

I/O Operations in Dylan

Learn input and output in Dylan - formatted console output with format-out, reading from standard input, and reading and writing files with streams and with-open-file

Input and output in Dylan live in the io library, and they reflect the language’s Lisp heritage: everything is built around stream objects. A stream is just an object you read elements from or write elements to, and the same functions — read-line, write-line, read-to-end — work whether the stream is the console, a file, or an in-memory buffer. Console output has its own convenience layer, the format-out function, which you have already met in the Hello World page.

Because Dylan organizes code into libraries and modules, I/O also brings a practical lesson about the module system. The format-out function comes from the format-out module, but reading input and working with files needs the streams and standard-io modules. A source file only sees the names its module explicitly imports, so a program that does more than print needs a slightly richer library definition than the one-line Hello World used.

In this tutorial you’ll go beyond a single format-out call: you’ll format numbers in several radixes, read lines from standard input, and both write and read files using the with-open-file macro that opens a stream and reliably closes it again. Along the way you’ll see how Dylan’s uniform stream model makes file I/O look almost identical to console I/O.

Formatted Console Output

The format-out function takes a control string containing directives — each starts with % — followed by the values that fill them in. It is Dylan’s printf, and it is available by default in the simple single-file project the Docker image builds, so this example runs directly.

Create a file named hello.dylan:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Module: hello

// Plain text; \n is an escaped newline inside the string literal.
format-out("=== Formatted Output in Dylan ===\n");

// %s inserts a value using its "message" representation — ideal for strings.
let language = "Dylan";
format-out("Language: %s\n", language);

// %d formats an integer in decimal. Extra arguments are consumed left to right.
let year = 1992;
format-out("%s first appeared in %d\n", language, year);

// The same integer can be shown in other radixes:
// %b binary, %o octal, %x hexadecimal.
let value = 255;
format-out("%d in binary is %b, octal %o, hex %x\n", value, value, value, value);

// %c prints a single character object (character literals use single quotes).
format-out("First letter: %c\n", 'D');

// %= prints the full inspect-style representation of ANY object,
// which is the easiest way to display collections like lists.
let primes = #(2, 3, 5, 7, 11);
format-out("Primes: %=\n", primes);

// %% emits a literal percent sign.
format-out("Test coverage: 100%%\n");
  • %s uses an object’s message form; for a string that is just the text itself.
  • %d, %b, %o, %x print an integer in decimal, binary, octal, and hexadecimal — note the value is passed once per directive.
  • %c prints a <character> such as the literal 'D'.
  • %= prints the same representation the debugger shows, so #(2, 3, 5, 7, 11) appears literally — perfect for lists and other collections.
  • %% is how you get a literal % into the output.

Setting Up a Project for Input and Files

Reading input and touching files needs names from the streams and standard-io modules, so those examples require a small project rather than a lone source file. A Dylan project is three files: a library/module definition, a .lid file listing the sources, and the source itself.

Create a file named library.dylan:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Module: dylan-user

define library io-demo
  use common-dylan;
  use io;
end library;

define module io-demo
  use common-dylan;
  use format-out;
  use streams;
  use standard-io;
end module;

Create a file named io-demo.lid:

1
2
3
Library: io-demo
Files: library
       io-demo

The use io; line pulls in the I/O library; the module’s use streams; and use standard-io; then expose read-line, write-line, with-open-file, and the standard stream variables. The source examples below all begin with Module: io-demo so they compile inside this project. Build them with the compiler bundled in the image (see Running with Docker).

Reading from Standard Input

*standard-input* and *standard-output* are the console streams from the standard-io module. read-line pulls one line of text — without the trailing newline — and input always arrives as a string, so convert it with string-to-integer when you need a number. Flushing the prompt with force-output makes sure it appears before the program blocks waiting for the user.

Create a file named io-demo.dylan:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Module: io-demo

// Print a prompt, then flush so it shows before we block on input.
format-out("What is your name? ");
force-output(*standard-output*);

// read-line returns the typed line as a string (newline stripped).
let name = read-line(*standard-input*);
format-out("Hello, %s! Nice to meet you.\n", name);

format-out("How old are you? ");
force-output(*standard-output*);

// Input is text, so convert it before doing arithmetic.
let age-text = read-line(*standard-input*);
let age = string-to-integer(age-text);
format-out("Next year you will be %d.\n", age + 1);
  • *standard-input* / *standard-output* are ordinary stream objects, so the same read-line/write functions work on them as on files.
  • read-line returns the line as a <string> with the newline removed.
  • force-output flushes buffered output — important for prompts that must appear before input is read.
  • string-to-integer (from common-dylan) parses text into an <integer>; there are matching converters like string-to-float.

Given the input Ada and 36 (the lines you type are shown after each prompt), this program prints:

What is your name? Ada
Hello, Ada! Nice to meet you.
How old are you? 36
Next year you will be 37.

Reading and Writing Files

File access uses the same streams, obtained from the with-open-file macro. It opens a stream bound to a name, runs the body, and guarantees the stream is closed afterwards — even if the body signals an error. The direction: keyword chooses #"output" (write) or #"input" (read).

This example reuses the same project. Create a file named io-demo.dylan with these contents:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Module: io-demo

// --- Writing a file ---
// with-open-file opens the stream and closes it when the body finishes.
with-open-file (stream = "poem.txt", direction: #"output")
  write-line(stream, "Roses are red,");
  write-line(stream, "Dylan dispatches on all,");
  write-line(stream, "Types picked at run time.");
end;
format-out("Wrote poem.txt\n");

// --- Reading a file line by line ---
// read-line signals an error at end of file unless you supply
// on-end-of-stream:, which is returned instead. Here we use #f as the sentinel.
with-open-file (stream = "poem.txt", direction: #"input")
  let line-number = 1;
  let line = read-line(stream, on-end-of-stream: #f);
  while (line)
    format-out("%d: %s\n", line-number, line);
    line-number := line-number + 1;
    line := read-line(stream, on-end-of-stream: #f);
  end while;
end;

// --- Reading the whole file at once ---
// with-open-file returns the value of its body; read-to-end returns the
// remaining contents as a string.
let contents = with-open-file (stream = "poem.txt", direction: #"input")
                 read-to-end(stream)
               end;
format-out("File is %d characters long.\n", contents.size);
  • with-open-file (stream = "poem.txt", direction: #"output") creates a writable file stream and closes it automatically at the end of the body.
  • write-line writes a string followed by a newline; there is also write for output without the newline and new-line for just a line break.
  • read-line(stream, on-end-of-stream: #f) returns #f at end of file instead of signalling an error, which lets the while loop stop cleanly.
  • := reassigns the line binding on each iteration — let bindings in Dylan are mutable.
  • read-to-end slurps the rest of the stream into a string; contents.size (method-call syntax for size(contents)) reports its length.

This program prints:

Wrote poem.txt
1: Roses are red,
2: Dylan dispatches on all,
3: Types picked at run time.
File is 66 characters long.

Running with Docker

The single-file console example runs with the documented one-liner — the image finds hello.dylan, wraps it in a project, compiles it, and runs the result:

1
2
3
4
5
# Pull the Dylan image
docker pull codearchaeology/dylan:latest

# With hello.dylan in the current directory:
docker run --rm -v $(pwd):/app codearchaeology/dylan:latest

The input and file examples are full projects (library.dylan, io-demo.lid, io-demo.dylan in the current directory). Build them with the dylan-compiler that ships in the image by pointing a registry at the .lid file — the same mechanism the image uses internally. The -it flags keep standard input open for the interactive example:

1
2
3
4
5
6
7
8
9
# Compile and run the io-demo project (library.dylan, io-demo.lid, io-demo.dylan)
docker run --rm -it -v $(pwd):/app --entrypoint bash codearchaeology/dylan:latest -c '
  mkdir -p /tmp/registry/x86_64-linux
  echo "/app/io-demo.lid" > /tmp/registry/x86_64-linux/io-demo
  export OPEN_DYLAN_USER_REGISTRIES=/tmp/registry
  cd /app
  dylan-compiler -build io-demo
  ./_build/bin/io-demo
'

Expected Output

Running the console example (hello.dylan):

=== Formatted Output in Dylan ===
Language: Dylan
Dylan first appeared in 1992
255 in binary is 11111111, octal 377, hex ff
First letter: D
Primes: #(2, 3, 5, 7, 11)
Test coverage: 100%

Key Concepts

  • Everything is a stream — the console (*standard-input*, *standard-output*) and files are all stream objects, so the same functions (read-line, write-line, read-to-end) work across them.
  • format-out is Dylan’s printf — directives like %s, %d, %b, %o, %x, %c, and %= format values, and %% produces a literal percent sign.
  • %= prints any object’s inspect representation, making it the go-to directive for displaying collections such as lists and vectors.
  • The module system gates I/O — a source file must use streams; and use standard-io; to reach input and file operations, which is why they need a fuller library definition than a bare format-out program.
  • with-open-file opens and reliably closes a file stream, and returns the value of its body — even if an error is signalled inside it.
  • read-line with on-end-of-stream: returns a sentinel (often #f) at end of file instead of raising an error, which makes line-by-line loops straightforward.
  • Input is textread-line yields a <string>, so use converters like string-to-integer before doing arithmetic on user input.
  • force-output flushes buffered output, ensuring prompts appear before the program blocks waiting for input.

Running Today

All examples can be run using Docker:

docker pull codearchaeology/dylan:latest
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining