341: Buffered Stream — BufReader and BufWriter
Functional Programming
Tutorial
The Problem
Reading or writing one byte at a time with unbuffered I/O makes a system call for each operation — catastrophically slow for large files. BufReader and BufWriter add an in-memory buffer: reads fill the buffer in bulk (e.g., 8KB), and subsequent reads serve from the buffer without syscalls. Writers accumulate data in the buffer and flush in bulk. This optimization, crucial for text file processing and log writing, reduces system call overhead by orders of magnitude.
🎯 Learning Outcomes
BufReader::new(reader) to wrap any reader with a 8KB internal bufferBufWriter::new(writer) to buffer writes and flush in bulkBufRead::lines() — lazy, bufferedBufWriter drop may silently discard errors — call flush() explicitlyCode Example
//! 341: Buffered Stream
//!
//! Demonstrates buffered I/O and string building in Rust using `BufReader`,
//! `BufWriter`, and `String` as analogs to OCaml's buffered channels and
//! `Buffer` module.
use std::io::{self, BufRead, BufReader, BufWriter, Read, Write};
// ---------------------------------------------------------------------------
// Approach 1: Buffered input — read all lines from a reader
// ---------------------------------------------------------------------------
/// Reads every line from `input` through a `BufReader`, returning them
/// in order. Mirrors OCaml's `read_lines_buffered`.
pub fn read_lines_buffered<R: Read>(input: R) -> io::Result<Vec<String>> {
BufReader::new(input).lines().collect()
}
// ---------------------------------------------------------------------------
// Approach 2: Buffered output — write lines through a BufWriter
// ---------------------------------------------------------------------------
/// Writes each line followed by `'\n'` into `output` via a `BufWriter`,
/// flushing at the end. Mirrors OCaml's `write_lines_buffered`.
pub fn write_lines_buffered<W: Write>(output: W, lines: &[&str]) -> io::Result<()> {
let mut writer = BufWriter::new(output);
for line in lines {
writeln!(writer, "{}", line)?;
}
writer.flush()
}
// ---------------------------------------------------------------------------
// Approach 3: String buffer — build a single string from many parts
// ---------------------------------------------------------------------------
/// Builds a newline-terminated string from `parts`, preallocating capacity
/// to avoid repeated allocations. Mirrors OCaml's `Buffer`-backed
/// `build_string`.
pub fn build_string(parts: &[&str]) -> String {
let capacity = parts.iter().map(|s| s.len() + 1).sum();
let mut buf = String::with_capacity(capacity);
for part in parts {
buf.push_str(part);
buf.push('\n');
}
buf
}
/// Concatenates `parts` end-to-end without separators. Matches the second
/// OCaml test's `Buffer.add_string` behavior.
pub fn concat_parts(parts: &[&str]) -> String {
let capacity = parts.iter().map(|s| s.len()).sum();
let mut buf = String::with_capacity(capacity);
for part in parts {
buf.push_str(part);
}
buf
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_string_joins_with_newlines() {
let result = build_string(&["hello", "world", "test"]);
assert_eq!(result, "hello\nworld\ntest\n");
}
#[test]
fn build_string_empty() {
assert_eq!(build_string(&[]), "");
}
#[test]
fn build_string_single() {
assert_eq!(build_string(&["only"]), "only\n");
}
#[test]
fn concat_parts_matches_buffer_add_string() {
assert_eq!(concat_parts(&["abc", "def"]), "abcdef");
}
#[test]
fn concat_parts_empty() {
assert_eq!(concat_parts(&[]), "");
}
#[test]
fn read_lines_buffered_roundtrip() {
let input = b"alpha\nbeta\ngamma\n" as &[u8];
let lines = read_lines_buffered(input).unwrap();
assert_eq!(lines, vec!["alpha", "beta", "gamma"]);
}
#[test]
fn read_lines_buffered_empty() {
let input: &[u8] = b"";
let lines = read_lines_buffered(input).unwrap();
assert!(lines.is_empty());
}
#[test]
fn read_lines_buffered_no_trailing_newline() {
let input = b"one\ntwo" as &[u8];
let lines = read_lines_buffered(input).unwrap();
assert_eq!(lines, vec!["one", "two"]);
}
#[test]
fn write_lines_buffered_produces_newline_terminated_output() {
let mut out: Vec<u8> = Vec::new();
write_lines_buffered(&mut out, &["hello", "world"]).unwrap();
assert_eq!(String::from_utf8(out).unwrap(), "hello\nworld\n");
}
#[test]
fn write_lines_buffered_empty_is_empty() {
let mut out: Vec<u8> = Vec::new();
write_lines_buffered(&mut out, &[]).unwrap();
assert!(out.is_empty());
}
#[test]
fn write_then_read_roundtrip() {
let lines = ["first", "second", "third"];
let mut buf: Vec<u8> = Vec::new();
write_lines_buffered(&mut buf, &lines).unwrap();
let read_back = read_lines_buffered(buf.as_slice()).unwrap();
assert_eq!(read_back, lines);
}
}Key Differences
In_channel / Out_channel are buffered by default; Rust's File is unbuffered — BufReader/BufWriter must be added explicitly.BufWriter's Drop implementation calls flush(), but ignores errors — always call flush() explicitly when error handling matters.BufReader::with_capacity(size) for tuned buffer sizes.tokio::io::BufReader / BufWriter are the async-aware equivalents for use with Tokio async I/O traits.OCaml Approach
OCaml's In_channel uses buffered I/O by default. In_channel.input_line is the standard line-by-line reader:
let count_lines path =
let ic = In_channel.open_text path in
let count = ref 0 in
(try while true do ignore (In_channel.input_line_exn ic); incr count done
with End_of_file -> ());
In_channel.close ic; !count
Full Source
//! 341: Buffered Stream
//!
//! Demonstrates buffered I/O and string building in Rust using `BufReader`,
//! `BufWriter`, and `String` as analogs to OCaml's buffered channels and
//! `Buffer` module.
use std::io::{self, BufRead, BufReader, BufWriter, Read, Write};
// ---------------------------------------------------------------------------
// Approach 1: Buffered input — read all lines from a reader
// ---------------------------------------------------------------------------
/// Reads every line from `input` through a `BufReader`, returning them
/// in order. Mirrors OCaml's `read_lines_buffered`.
pub fn read_lines_buffered<R: Read>(input: R) -> io::Result<Vec<String>> {
BufReader::new(input).lines().collect()
}
// ---------------------------------------------------------------------------
// Approach 2: Buffered output — write lines through a BufWriter
// ---------------------------------------------------------------------------
/// Writes each line followed by `'\n'` into `output` via a `BufWriter`,
/// flushing at the end. Mirrors OCaml's `write_lines_buffered`.
pub fn write_lines_buffered<W: Write>(output: W, lines: &[&str]) -> io::Result<()> {
let mut writer = BufWriter::new(output);
for line in lines {
writeln!(writer, "{}", line)?;
}
writer.flush()
}
// ---------------------------------------------------------------------------
// Approach 3: String buffer — build a single string from many parts
// ---------------------------------------------------------------------------
/// Builds a newline-terminated string from `parts`, preallocating capacity
/// to avoid repeated allocations. Mirrors OCaml's `Buffer`-backed
/// `build_string`.
pub fn build_string(parts: &[&str]) -> String {
let capacity = parts.iter().map(|s| s.len() + 1).sum();
let mut buf = String::with_capacity(capacity);
for part in parts {
buf.push_str(part);
buf.push('\n');
}
buf
}
/// Concatenates `parts` end-to-end without separators. Matches the second
/// OCaml test's `Buffer.add_string` behavior.
pub fn concat_parts(parts: &[&str]) -> String {
let capacity = parts.iter().map(|s| s.len()).sum();
let mut buf = String::with_capacity(capacity);
for part in parts {
buf.push_str(part);
}
buf
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_string_joins_with_newlines() {
let result = build_string(&["hello", "world", "test"]);
assert_eq!(result, "hello\nworld\ntest\n");
}
#[test]
fn build_string_empty() {
assert_eq!(build_string(&[]), "");
}
#[test]
fn build_string_single() {
assert_eq!(build_string(&["only"]), "only\n");
}
#[test]
fn concat_parts_matches_buffer_add_string() {
assert_eq!(concat_parts(&["abc", "def"]), "abcdef");
}
#[test]
fn concat_parts_empty() {
assert_eq!(concat_parts(&[]), "");
}
#[test]
fn read_lines_buffered_roundtrip() {
let input = b"alpha\nbeta\ngamma\n" as &[u8];
let lines = read_lines_buffered(input).unwrap();
assert_eq!(lines, vec!["alpha", "beta", "gamma"]);
}
#[test]
fn read_lines_buffered_empty() {
let input: &[u8] = b"";
let lines = read_lines_buffered(input).unwrap();
assert!(lines.is_empty());
}
#[test]
fn read_lines_buffered_no_trailing_newline() {
let input = b"one\ntwo" as &[u8];
let lines = read_lines_buffered(input).unwrap();
assert_eq!(lines, vec!["one", "two"]);
}
#[test]
fn write_lines_buffered_produces_newline_terminated_output() {
let mut out: Vec<u8> = Vec::new();
write_lines_buffered(&mut out, &["hello", "world"]).unwrap();
assert_eq!(String::from_utf8(out).unwrap(), "hello\nworld\n");
}
#[test]
fn write_lines_buffered_empty_is_empty() {
let mut out: Vec<u8> = Vec::new();
write_lines_buffered(&mut out, &[]).unwrap();
assert!(out.is_empty());
}
#[test]
fn write_then_read_roundtrip() {
let lines = ["first", "second", "third"];
let mut buf: Vec<u8> = Vec::new();
write_lines_buffered(&mut buf, &lines).unwrap();
let read_back = read_lines_buffered(buf.as_slice()).unwrap();
assert_eq!(read_back, lines);
}
}
✓ Tests
Rust test suite
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_string_joins_with_newlines() {
let result = build_string(&["hello", "world", "test"]);
assert_eq!(result, "hello\nworld\ntest\n");
}
#[test]
fn build_string_empty() {
assert_eq!(build_string(&[]), "");
}
#[test]
fn build_string_single() {
assert_eq!(build_string(&["only"]), "only\n");
}
#[test]
fn concat_parts_matches_buffer_add_string() {
assert_eq!(concat_parts(&["abc", "def"]), "abcdef");
}
#[test]
fn concat_parts_empty() {
assert_eq!(concat_parts(&[]), "");
}
#[test]
fn read_lines_buffered_roundtrip() {
let input = b"alpha\nbeta\ngamma\n" as &[u8];
let lines = read_lines_buffered(input).unwrap();
assert_eq!(lines, vec!["alpha", "beta", "gamma"]);
}
#[test]
fn read_lines_buffered_empty() {
let input: &[u8] = b"";
let lines = read_lines_buffered(input).unwrap();
assert!(lines.is_empty());
}
#[test]
fn read_lines_buffered_no_trailing_newline() {
let input = b"one\ntwo" as &[u8];
let lines = read_lines_buffered(input).unwrap();
assert_eq!(lines, vec!["one", "two"]);
}
#[test]
fn write_lines_buffered_produces_newline_terminated_output() {
let mut out: Vec<u8> = Vec::new();
write_lines_buffered(&mut out, &["hello", "world"]).unwrap();
assert_eq!(String::from_utf8(out).unwrap(), "hello\nworld\n");
}
#[test]
fn write_lines_buffered_empty_is_empty() {
let mut out: Vec<u8> = Vec::new();
write_lines_buffered(&mut out, &[]).unwrap();
assert!(out.is_empty());
}
#[test]
fn write_then_read_roundtrip() {
let lines = ["first", "second", "third"];
let mut buf: Vec<u8> = Vec::new();
write_lines_buffered(&mut buf, &lines).unwrap();
let read_back = read_lines_buffered(buf.as_slice()).unwrap();
assert_eq!(read_back, lines);
}
}
Deep Comparison
Core Insight
Buffering reduces system calls — wrapping raw I/O in buffers is essential for performance
OCaml Approach
Rust Approach
Comparison Table
| Feature | OCaml | Rust |
|---|---|---|
| See | example.ml | example.rs |
Exercises
BufReader — measure time and system call count.BufWriter with periodic explicit flushes every 1000 lines.BufReader::lines() to process a CSV file lazily, parsing each line into a Vec<String> of fields.