I/O Operations in Roc
Learn how to read input, write output, and work with files in Roc using its explicit effect model and Result-based error handling
Input and output are where a purely functional language has to be honest about the real world. In Roc, every function is pure by default — given the same arguments, it always returns the same result and changes nothing outside itself. Reading from a keyboard or writing to a file breaks that promise, so Roc makes those operations impossible to hide.
Roc’s answer is the platform/application split. The application code you write stays pure, while a platform (here, basic-cli) supplies the effectful building blocks: printing, reading input, and touching the filesystem. Any function that performs an effect is marked with a ! suffix, so Stdout.line!, Stdin.line!, and File.read_utf8! all announce their effects right in their names. When you call one, the ! shows up at the call site too — you can scan a block of Roc and see exactly where the outside world gets involved.
The second half of the story is error handling. Roc has no exceptions and no null. An operation that might fail returns a Result — either Ok with a value or Err with a reason. To keep effectful code readable, Roc provides the ? operator, which unwraps an Ok and short-circuits out of the function on an Err. This tutorial covers formatted console output, reading interactive input, writing and reading files, and handling I/O failures with Result.
Formatted Console Output
The Hello World page introduced Stdout.line!. Here we add Stdout.write! (which prints without a trailing newline) and use string interpolation to format values. Note how each intermediate effect ends with ? to sequence it and discard its {} result.
Create a file named console_output.roc:
app [main!] { cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br" }
import cli.Stdout
main! = |_args|
# write! prints without a trailing newline, so these join on one line
Stdout.write!("Loading")?
Stdout.write!("...")?
Stdout.line!(" done!")?
# String interpolation with ${...} formats values into text
name = "Roc"
version = 4
Stdout.line!("Language: ${name}")?
Stdout.line!("Alpha release: ${Num.to_str(version)}")?
# Fractional literals default to Dec, so this prints exactly
pi = 3.14159
Stdout.line!("Pi is about ${Num.to_str(pi)}")
The final expression in main! is the function’s return value, so it needs no ?. Every line before it uses ? to run the effect and continue.
Reading Input from the Console
Stdin.line! reads a single line from standard input and returns a Result Str _. The ? operator unwraps the Ok string; if the input stream ends unexpectedly, the Err propagates out of main! and the platform reports it.
Create a file named read_input.roc:
app [main!] { cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br" }
import cli.Stdout
import cli.Stdin
main! = |_args|
Stdout.line!("What is your name?")?
name = Stdin.line!({})?
Stdout.line!("What is your favorite language?")?
language = Stdin.line!({})?
Stdout.line!("Nice to meet you, ${name}!")?
Stdout.line!("${language} is a great choice!")
Because Stdin.line! returns a Result, there is no way to accidentally use unread or failed input — the type system forces you to deal with it, either by unwrapping with ? or by pattern matching.
Writing and Reading Files
The File module provides File.write_utf8! (which takes the path first, then the contents) and File.read_utf8! (which takes a path and returns the file’s contents). Both return a Result, so both compose cleanly with ?.
Create a file named file_io.roc:
app [main!] { cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br" }
import cli.Stdout
import cli.File
main! = |_args|
contents = "Roc makes I/O effects explicit.\nEvery effectful function ends with !\n"
# Write the string to a file, creating or overwriting it
File.write_utf8!("notes.txt", contents)?
Stdout.line!("Wrote notes.txt")?
# Read the file back into a string
loaded = File.read_utf8!("notes.txt")?
Stdout.line!("File contents:")?
# loaded already ends in a newline, so use write! to avoid a blank line
Stdout.write!(loaded)
Handling I/O Errors
Not every file exists, and not every read succeeds. Instead of unwrapping with ?, you can pattern match on the Result with when ... is to handle both outcomes explicitly. The compiler enforces that you cover every case, so a forgotten error branch is a compile error, not a runtime crash.
Create a file named io_errors.roc:
app [main!] { cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br" }
import cli.Stdout
import cli.File
main! = |_args|
# read_utf8! returns a Result rather than throwing — we handle both cases
result = File.read_utf8!("missing.txt")
when result is
Ok(contents) ->
Stdout.line!("File says: ${contents}")
Err(_) ->
Stdout.line!("Could not read missing.txt — using a default value")
Since missing.txt does not exist, the read fails and control flows into the Err branch. The underscore _ in Err(_) discards the specific error reason; you could bind it to a name instead to report exactly what went wrong.
Running with Docker
| |
Note: The
read_input.rocexample needs the-itflags so Docker keeps an interactive terminal open for typed input. On the first run, Roc downloads thebasic-cliplatform, which may take a few seconds.
Expected Output
Running console_output.roc:
Loading... done!
Language: Roc
Alpha release: 4
Pi is about 3.14159
Running read_input.roc (lines you type are shown after each prompt):
What is your name?
Ada
What is your favorite language?
Roc
Nice to meet you, Ada!
Roc is a great choice!
Running file_io.roc:
Wrote notes.txt
File contents:
Roc makes I/O effects explicit.
Every effectful function ends with !
Running io_errors.roc:
Could not read missing.txt — using a default value
Key Concepts
- Effects are marked with
!—Stdout.line!,Stdin.line!,File.read_utf8!, and every other effectful call ends in!, so I/O is visible everywhere it happens. - The platform provides I/O — your application stays pure; the
basic-cliplatform supplies the actual reading, writing, and printing. write!vsline!—Stdout.write!prints text as-is, whileStdout.line!appends a newline.- The
?operator sequences effects — it unwraps anOkvalue and short-circuits out of the function on anErr, keeping effectful code flat and readable. - I/O returns
Result, never throws — Roc has no exceptions; failures areErrvalues you must handle, either with?or by pattern matching. when ... isis exhaustive — the compiler requires you to cover bothOkandErr, so unhandled error cases are caught at compile time.- String interpolation formats output —
${...}embeds values into strings, withNum.to_strconverting numbers to text. - File writes take the path first —
File.write_utf8!(path, contents)creates or overwrites the file atpath.
Running Today
All examples can be run using Docker:
docker pull roclang/nightly-ubuntu-2204:latest
Comments
Loading comments...
Leave a Comment