Intermediate

I/O Operations in Vale

Learn console output, reading from stdin, and file reading and writing in Vale using the stdlib path and stdin modules, with Docker-ready examples

Input and output are where a program meets the outside world, and they are also where Vale’s alpha status shows most clearly. As an imperative systems language, Vale handles I/O the way C or Rust would — direct function calls that read and write immediately, no effect system or I/O monad in sight. What Vale 0.2 ships is deliberately small: print and println for console output, a stdlib.stdin module for keyboard input, and a stdlib.path module whose Path type handles file reading, writing, and filesystem queries.

The interesting part is what Vale’s memory model does not add here. Because safety comes from single ownership plus runtime generational references rather than a borrow checker, passing strings to I/O functions and holding the results requires no lifetime annotations — file contents come back as an ordinary owned str you can use like any other value. The trade-off in version 0.2 is minimal error handling: most file operations either succeed or terminate the program, so defensive checks like exists() do the work that Result types do in more mature languages.

In this tutorial you will compose formatted console output, read integers from standard input, write and read back a text file through the Path API, and guard file reads against missing files. Each example is a complete program you can compile and run with Docker.

Console Output: print, println, and Building Lines

You met println in Hello World; the fuller picture is that Vale’s standard library gives you two output functions with several overloads. print writes text without a trailing newline, println adds one, and both accept int and bool values directly in addition to str. For anything more elaborate, you build the line yourself with the + concatenation operator and str(...) conversions — Vale 0.2 has no printf-style formatter, so string concatenation is the formatting facility.

Create a file named io_print.vale:

import stdlib.*;

exported func main() {
  // print writes without a newline; println ends the line
  print("Loading");
  print("...");
  println("done");

  // println accepts int and bool values directly
  println(42);
  println(true);

  // Formatted output is built with + and str()
  rows = 3;
  cols = 4;
  println("grid: " + str(rows) + " x " + str(cols) + " = " + str(rows * cols) + " cells");

  // \n embeds a newline inside a single string literal
  println("first line\nsecond line");
}

The three print calls produce a single line of output, finished by println("done"). The last statement shows that one println call can emit multiple lines: the \n escape sequence inside the literal is a real newline character. Under the hood the stdlib’s println is just print(s + "\n"), so everything ultimately funnels through C’s printf on standard output.

Console Input: Reading from stdin

Keyboard input lives in its own module, stdlib.stdin, which you import alongside the main standard library. Vale 0.2 provides two functions: stdinReadInt(), which skips whitespace and parses the next integer from standard input (it wraps C’s scanf("%d", ...)), and getch(), which returns a single raw character as an int without waiting for Enter. There is no string-reading readLine in this release, so integer input is the practical workhorse.

Create a file named io_input.vale:

import stdlib.*;
import stdlib.stdin.*;

exported func main() {
  println("Enter two integers:");
  a = stdinReadInt();
  b = stdinReadInt();
  println("a + b = " + str(a + b));
  println("a * b = " + str(a * b));
}

Because stdinReadInt skips over spaces and newlines before parsing, the two calls happily consume input given as 6 7 on one line or on two separate lines. The values arrive as ordinary int bindings, ready for arithmetic. When you run this in Docker you pipe the input in — see the Running with Docker section for the -i flag this requires.

File Writing and Reading with Path

File I/O goes through the Path struct from stdlib.path. You construct one with Path("some/file.txt"), and the resulting value carries methods for the common operations: writeString(contents) creates or overwrites the file with the given string, readAsString() returns the entire file contents as a str, and name() gives the final path segment. There are no open/close steps and no file handle to manage — each call opens the file, does its work, and closes it before returning, and the Path value itself is cleaned up by Vale’s single-ownership model when it goes out of scope.

Create a file named io_files.vale:

import stdlib.*;
import stdlib.path.*;

exported func main() {
  // writeString creates the file, or overwrites it if it exists
  journal = Path("journal.txt");
  journal.writeString("Day 1: found the ruins\nDay 2: deciphered the tablets\n");
  println("wrote " + journal.name());

  // readAsString returns the whole file as one str
  contents = journal.readAsString();
  print(contents);
}

The program writes two lines into journal.txt, announces the file name, then reads the file straight back and prints it. Note the final call is print, not println — the file contents already end with a newline, so printing them verbatim reproduces the two lines exactly. The journal binding owns the Path value, and the methods borrow it, which is why the same binding can be used for both the write and the read.

Guarding Against Missing Files

Vale 0.2’s file API has a sharp edge: readAsString() on a file that does not exist prints an error and terminates the process — there is no exception to catch and no Result to inspect on the read path. The idiomatic guard in this release is to ask before you read: exists() reports whether the path is present, and is_file() distinguishes regular files from directories. Wrapping the check-then-read pattern in a small function keeps the call sites tidy.

Create a file named io_safety.vale:

import stdlib.*;
import stdlib.path.*;

// Read and report a file only if it actually exists
func describe(path_str str) {
  p = Path(path_str);
  if p.exists() {
    println(path_str + " exists (" + str(p.readAsString().len()) + " bytes)");
  } else {
    println(path_str + " is missing; skipping read");
  }
}

exported func main() {
  Path("notes.txt").writeString("generational references\n");

  describe("notes.txt");
  describe("missing.txt");
}

The first describe call finds the file just written and reports its size — readAsString() hands back a str, and len() counts its bytes. The second call hits the else branch and skips the read entirely, which is exactly what saves the program from being terminated by a failed read. This check-first style is a pragmatic workaround for an alpha-stage standard library; a future Vale would likely return Result values here, as its directory-creation function CreateDirAll already does.

Running with Docker

There is no official Vale image on Docker Hub, so we use the custom codearchaeology/vale:0.2 image (it bundles valec, a JDK for the Scala frontend, and clang for linking).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Pull the custom Vale image
docker pull codearchaeology/vale:0.2

# Console output
docker run --rm -v $(pwd):/app -w /app codearchaeology/vale:0.2 bash -c '/opt/vale/valec build mymod=io_print.vale --output_dir build && ./build/main'

# Console input — note the -i flag, which connects your stdin to the container
echo "6 7" | docker run --rm -i -v $(pwd):/app -w /app codearchaeology/vale:0.2 bash -c '/opt/vale/valec build mymod=io_input.vale --output_dir build && ./build/main'

# File writing and reading
docker run --rm -v $(pwd):/app -w /app codearchaeology/vale:0.2 bash -c '/opt/vale/valec build mymod=io_files.vale --output_dir build && ./build/main'

# Guarded file reads
docker run --rm -v $(pwd):/app -w /app codearchaeology/vale:0.2 bash -c '/opt/vale/valec build mymod=io_safety.vale --output_dir build && ./build/main'

The input example pipes 6 7 into the container; the -i flag is required so the container’s standard input stays open. You can also run it interactively (drop the echo pipe and add -t) and type the numbers yourself. The file examples create journal.txt and notes.txt inside the mounted directory, so you will find them next to your source files afterward.

As with the earlier tutorials, the Vale compiler prints verbose build progress (frontend, backend, and linker stages) before your program runs — your program’s output is the final lines.

Expected Output

For io_print.vale:

Loading...done
42
true
grid: 3 x 4 = 12 cells
first line
second line

For io_input.vale (with 6 7 piped in):

Enter two integers:
a + b = 13
a * b = 42

For io_files.vale:

wrote journal.txt
Day 1: found the ruins
Day 2: deciphered the tablets

For io_safety.vale:

notes.txt exists (24 bytes)
missing.txt is missing; skipping read

Key Concepts

  • Two output functions, several overloadsprint writes without a newline, println adds one, and both accept str, int, and bool; println(s) is literally print(s + "\n") in the standard library.
  • Concatenation is the formatter — Vale 0.2 has no printf equivalent; formatted lines are assembled with + and str(...) conversions, and \n escapes embed line breaks inside literals.
  • stdin is a separate module — console input requires import stdlib.stdin.*;, which provides stdinReadInt() (whitespace-skipping integer parsing) and getch() (one raw character); there is no string readLine in this release.
  • Files go through Path — construct with Path("file.txt"), then use writeString(contents), readAsString(), exists(), is_file(), and name(); there are no explicit open/close steps or file handles to manage.
  • Whole-file reads and writesreadAsString() returns the entire file as one owned str and writeString() replaces the entire file; Vale 0.2 has no streaming or append API, so line-by-line processing means splitting the string after reading.
  • Check before you read — a failed readAsString() terminates the program rather than returning an error, so guarding reads with exists() is the idiomatic Vale 0.2 pattern for I/O error handling.
  • Ownership without ceremony — a Path or file-contents str is an ordinary owned value: methods borrow it, generational references keep access safe at runtime, and no lifetime annotations are needed.
  • Docker needs -i for stdin — when a program reads standard input, run the container with docker run -i (or pipe input in) so stdin reaches your program.

Running Today

All examples can be run using Docker:

docker pull codearchaeology/vale:0.2
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining