I/O Operations in OCaml
Learn input and output in OCaml - formatted console output with Printf, reading from stdin, file reading and writing with channels, and exception-based error handling
Input and output are where OCaml’s functional core meets the messy, stateful outside world. Unlike Haskell, OCaml does not wrap I/O in a monad - it takes a pragmatic, multi-paradigm stance. I/O functions like print_endline and input_line are ordinary functions that perform side effects and return unit (or a value read from the outside), and you sequence them with the humble semicolon.
OCaml’s I/O model is built around channels: an out_channel for writing and an in_channel for reading. The standard channels stdout, stderr, and stdin are always available, and you open your own channels for files. Layered on top of raw channel functions is the Printf module, which provides type-safe, C-style formatted output - the format string is checked at compile time, so %d really does require an int.
In this tutorial you will go beyond the single print_endline from Hello World: formatted output with Printf, reading interactive input from stdin, writing and reading files through channels, and handling the exceptions (End_of_file, Sys_error) that I/O naturally raises. Because errors are values and exceptions in OCaml, you will see how option types and try ... with combine to make I/O safe.
Formatted Console Output
The Printf module gives you format specifiers with full type checking. The compiler reads the format string and requires arguments of matching types.
Create a file named output.ml:
| |
The format string is not just a string - it is a special format type. This is why Printf.printf "%d" "oops" fails to compile: the %d demands an int, and the type checker enforces it before your program ever runs.
Reading Console Input
Reading from stdin uses read_line (returns a string) and helpers like read_int (reads a line and parses it as an int). These functions perform side effects and return the value read.
Create a file named input.ml:
| |
read_line () returns the next line from stdin without its trailing newline. read_int () reads a line and converts it with int_of_string, raising Failure if the text is not a valid integer. Note the () argument: these are functions that take unit, forcing the read to happen when called rather than when defined.
Writing and Reading Files
File I/O opens a channel, works with it, then closes it. open_out creates (or truncates) a file for writing; open_in opens one for reading. Reading line by line pairs naturally with pattern matching, including OCaml’s exception patterns that let you match End_of_file right inside a match.
Create a file named file_io.ml:
| |
The | exception End_of_file -> branch is idiomatic OCaml: input_line raises End_of_file when there is nothing left to read, and the exception pattern handles it as cleanly as a normal case. Combining the read with List.fold_left shows how imperative I/O feeds directly into functional data processing.
Handling I/O Errors Safely
I/O can fail: a file may not exist, or a disk may be full. OCaml signals these with exceptions such as Sys_error. A common pattern is to wrap the risky operation in try ... with and return an option, turning a possible failure into a value the caller must handle.
Create a file named io_errors.ml:
| |
In_channel.input_all reads an entire channel into a single string. The Sys_error from the missing file is caught, reported on stderr with Printf.eprintf, and converted to None - so the program continues instead of crashing. Returning option forces the caller to explicitly handle the failure case via pattern matching.
Running with Docker
| |
The volume mount means files written inside the container (like inventory.csv and greeting.txt) appear in your current directory after the run.
Expected Output
Running output.ml:
Integer: 42
Float: 3.14
String: OCaml
Char: A
Bool: true
Hex: ff
OCaml is 29 years old
2 + 3 = 5
|left | 7|
Running input.ml with the piped input Alice and 30:
Enter your name: Enter your age: Hello, Alice!
Next year you will be 31
(The prompts appear before the answers because buffered stdout is flushed together at exit; the piped input is consumed, not echoed.)
Running file_io.ml:
Wrote inventory.csv
Read 3 lines:
Apples,10
Bananas,5
Cherries,20
Total items: 35
Running io_errors.ml:
Error: does_not_exist.txt: No such file or directory
Could not read the file
Content: Hello from a safe read!
Key Concepts
- Channels are the foundation:
out_channelandin_channelmodel output and input, withstdout,stderr, andstdinalways available andopen_out/open_infor files. Printfis type-safe: format strings are checked at compile time, so%drequires anintand%srequires astring- a mismatch is a compile error, not a runtime surprise.- I/O returns
unit: side-effecting output functions return(), and you sequence them with semicolons rather than a monad. - Reading takes
unit: functions likeread_line ()andread_int ()take a()argument so the read happens when called, andread_intcan raiseFailureon bad input. - Exceptions signal end and failure:
input_lineraisesEnd_of_file, and file operations raiseSys_error; OCaml’s| exception ... ->patterns handle these directly inside amatch. - Convert exceptions to
option: wrapping risky I/O intry ... withand returningSome/Nonemakes failure a value the caller must handle explicitly. - Always close channels: pair
open_outwithclose_outandopen_inwithclose_into flush buffers and release file handles. - I/O feeds functional pipelines: imperative reads combine naturally with
List.fold_left,List.iter, andString.split_on_charfor processing the data you read.
Comments
Loading comments...
Leave a Comment