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).
| |
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 overloads —
printwrites without a newline,printlnadds one, and both acceptstr,int, andbool;println(s)is literallyprint(s + "\n")in the standard library. - Concatenation is the formatter — Vale 0.2 has no printf equivalent; formatted lines are assembled with
+andstr(...)conversions, and\nescapes embed line breaks inside literals. - stdin is a separate module — console input requires
import stdlib.stdin.*;, which providesstdinReadInt()(whitespace-skipping integer parsing) andgetch()(one raw character); there is no stringreadLinein this release. - Files go through
Path— construct withPath("file.txt"), then usewriteString(contents),readAsString(),exists(),is_file(), andname(); there are no explicit open/close steps or file handles to manage. - Whole-file reads and writes —
readAsString()returns the entire file as one ownedstrandwriteString()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 withexists()is the idiomatic Vale 0.2 pattern for I/O error handling. - Ownership without ceremony — a
Pathor file-contentsstris an ordinary owned value: methods borrow it, generational references keep access safe at runtime, and no lifetime annotations are needed. - Docker needs
-ifor stdin — when a program reads standard input, run the container withdocker 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
Comments
Loading comments...
Leave a Comment