I/O Operations in F#
Learn console output, reading input, and file I/O in F# with formatted printing, pipelines, and functional error handling using Docker-ready examples
Input and output are where a program meets the outside world. In Hello World you saw a single printfn call; here we go further — formatting values for the console, reading input from the user, and reading and writing files.
F# is functional-first, so its I/O has a distinctive flavour. Under the hood, file I/O comes straight from the .NET System.IO namespace, which means you get the entire Base Class Library. On top of that, F# adds its own type-safe formatting functions (printfn, sprintf, eprintfn) whose format strings are checked at compile time — a mismatched %d or %s is a compiler error, not a runtime surprise.
Because I/O is inherently effectful (it reads from and writes to the world outside your program), F# treats it as ordinary code rather than hiding it behind a monad the way Haskell does. What stays functional is how you shape the data around the I/O: pipelines (|>), pattern matching on results, and try/with for errors. This tutorial shows console output, console input, file reading and writing, and error handling — all runnable in Docker.
Formatted Console Output
F#’s printf family gives you type-checked, printf-style formatting. Create a file named io_output.fsx:
| |
Key format specifiers: %s (string), %d (integer), %f/%.3f (float with precision), %b (boolean), %A (any value, structurally), and %% for a literal percent sign. Because the compiler reads the format string, passing the wrong argument type is caught before the program ever runs.
Reading Console Input
To read from standard input, F# uses .NET’s System.Console. Create a file named io_input.fsx:
| |
Console.ReadLine() returns the next line from stdin as a string. Rather than throwing when the year isn’t a number, Int32.TryParse hands back a tuple that pattern matching destructures cleanly — a common F# alternative to try/catch for parsing.
Writing and Reading Files
The System.IO.File type provides simple one-call helpers for whole-file access. Create a file named io_files.fsx:
| |
WriteAllText and WriteAllLines handle opening, writing, and closing the file for you — there is no file handle to manage or forget to close. Once the data is in memory, F# pipelines (|>) and functions like Array.iteri let you process it in a functional style.
Handling I/O Errors
File operations can fail — a file may be missing, locked, or unreadable. F# handles these with try/with and pattern matching on exception types. Create a file named io_errors.fsx:
| |
The :? FileNotFoundException syntax is a type-test pattern: it matches when the caught exception is of that type. FileNotFoundException is a subtype of IOException, so listing the more specific case first ensures it takes priority. When you’d rather avoid exceptions entirely, File.Exists lets you branch before touching the disk.
Running with Docker
| |
Expected Output
Running io_output.fsx:
Name: Ada, Age: 36
Pi is approximately 3.142
Enabled: true
No newline here... continued on the same line
Grace scored 95%
F# first appeared in 2005
Numbers: [1; 2; 3]
Running io_input.fsx with the piped answers Ada and 2026 (the prompts print without a newline, and piped input is not echoed):
What is your name? What year is it? Hello, Ada! Next year will be 2027.
Running io_files.fsx:
--- notes.txt ---
First line
Second line
Appended line
--- languages.txt (3 lines) ---
0: F#
1: OCaml
2: Haskell
Running io_errors.fsx:
Read 5 characters from present.txt
File not found: missing.txt
present.txt exists
missing.txt is missing
Key Concepts
- Type-checked formatting —
printfn,printf, andsprintfvalidate their format strings at compile time, so a%d/%smismatch fails to build rather than misbehaving at runtime. printfvsprintfnvssprintf—printfomits the trailing newline,printfnadds one, andsprintfreturns the formatted string instead of printing it.- String interpolation —
$"{value}"(F# 5+) is a readable alternative to format specifiers for straightforward cases. - File I/O is .NET I/O —
System.IO.Filehelpers likeReadAllText,WriteAllText,WriteAllLines, andAppendAllTexthandle opening and closing for you, so there are no handles to leak. - Parse without exceptions —
Int32.TryParsereturns a(bool, int)tuple that pattern matching destructures, a common F# alternative to try/catch. - Errors via
try/with— the:? ExceptionTypetype-test pattern lets you match specific failures likeFileNotFoundException; list more specific exception types before their base types. - Pipelines around effects — I/O itself is effectful, but F# keeps the surrounding data transformations functional with
|>,List.map, andArray.iteri.
Running Today
All examples can be run using Docker:
docker pull mcr.microsoft.com/dotnet/sdk:9.0
Comments
Loading comments...
Leave a Comment