Imperative I/O performs an algorithm through explicit control flow, mutable state, iteration, and resource operations. It can be incremental and terminate early, but it mixes the algorithm with low-level resource-safety details. section 15.1 section 15.6
Counting lines without loading the file
A program checking whether a file exceeds 40,000 lines opens a source, obtains an iterator, increments a mutable counter inside a while loop, stops once the answer is known, and closes the source in finally. The approach keeps memory use low and supports early termination, but an omitted or misplaced close can leak a scarce file handle. section 15.1
Listing 15.1 Counting line numbers in imperative style
def linesGt40k(filename: String): IO[Boolean] = IO:
val src = io.Source.fromFile(filename) #1
try
var count = 0
val lines: Iterator[String] = src.getLines() #2
while count <= 40000 && lines.hasNext do
lines.next #3
count += 1
count > 40000
finally src.close
Use the listing to inspect how mutable state, incremental iteration, early termination, and finally-based cleanup combine in one IO action. section 15.1
Imperative I/O exposes several recurring tensions:
- Incremental processing avoids loading an entire file, while mutable iterators and loops entangle that behavior with the algorithm. section 15.1
- Resource safety requires closing files after normal completion or exceptions and avoiding reads after closure. section 15.1
- Streaming abstractions separate composable computation from these operational details and can preserve early termination, error handling, and effect evaluation. section 15.6
