Lazy I/O represents a file-backed stream as an IO[LazyList[String]], recovering composable stream operations while deferring reads until elements are forced. 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
The imperative alternative opens a file, advances an iterator, stops early at a limit, and closes the resource explicitly. section 15.1
A tempting lazy representation
A lines function can convert getLines() into a LazyList and append a deferred close operation. This supports operations such as filter, take, map, and indexOfSlice without reading the entire file immediately. section 15.1
The representation remains unsafe when stream traversal is not tightly controlled:
- Early termination can skip the appended close operation and leak the file resource. section 15.1
- Repeated traversal can retain all elements or attempt reads from a closed file, depending on memoization. section 15.1
- Concurrent forcing can produce unpredictable behavior because stream evaluation performs I/O effects. section 15.1
