So far, we’ve encountered a few different flavors of file-like objects. We’ve used the standard input, standard output, and anonymous pipes, as well as sockets. From an API perspective, we’ve worked with both the low-level int file descriptors used by the ANSI C core, as well as the higher-level Ptr[File] accepted by fgets, fprintf, and other standard POSIX I/O functions. All of these abstractions provide a notion of a stream: the plain int fd is an ordered series of bytes, retrieved with read(), whereas the Ptr[File] objects behave more like an ordered series of lines, retrieved by fgets().
Both of these abstraction layers try to abstract over the differences between sockets, pipes, and regular on-disk files, but not without a few pain points—after all, the underlying entities have fundamental differences. For example, pipes are unidirectional, meaning you can read from stdin, but not write, whereas you can write to stdin, but not read. Sockets, on the other hand, are bidirectional: once I have a TCP connection, I can read a request from it, and then write a response back on the same connection. Disk files behave differently still. Not only do they support simultaneous reads and writes, but files also have seekable position—I can rewind a file back to the beginning or jump ahead to any arbitrary byte.
All of which is to say that providing a safe, ergonomic interface over this functionality is difficult. The POSIX functions we used in the first half of the book are often awkward and prone to throw obscure errors. For example, what is the computer supposed to do if I ask it to seek() a socket back to its beginning? As Scala developers, we would hope, instead, to provide type-safe APIs that behave consistently, and catch misuse at compile time.
Fortunately, we don’t have to be quite as general as POSIX. The model we developed in the last chapter using a Future[T] to model an asynchronous request-response cycle, takes care of the typical use of a bidirectional socket nicely. And likewise, fully read-write, freely seeking file I/O is unusual, and probably left to low-level POSIX wrangling. What we are left with, then, is bulk, unidirectional, typed I/O over pipes and files.
In Scala, the most popular idioms for this sort of problem has a distinctly functional flavor. Scala has a plethora of fine functional streaming libraries, including monix,[40] cats-effect,[41] fs2,[42] zio,[43] and others. This book isn’t about functional programming, but even libuv biases us toward stateless, function-oriented processing. However, our focus will be on providing a natural, idiomatic wrapper for libuv’s capabilities that we can compose into powerful programs that solve real-world problems.