liveBook · Manning

Non-blocking I/O

As explained in Functional Programming in Scala, Second Edition

Non-blocking asynchronous I/O performs external input and output without waiting synchronously for each operation to complete.

Non-blocking I/O starts an operation and returns immediately instead of occupying the current thread until external input or output completes. A callback receives the result or error when the I/O subsystem is ready. section 13.5

A compositional interface hides the underlying callback-based library. An asynchronous action can be represented as Par[A], where a callback is invoked with the eventual A; Free[Par, A] then sequences such actions without exposing library-specific details. section 7.3.4 section 13.5

The design separates several concerns:

  • Represent completion with a callback rather than retrieving a result through a blocking Future.get. section 7.3.4
  • Adapt a callback-oriented source into Par with async, preserving success and failure in Either. section 13.5
  • Compose multiple reads through Free[Par, A], so programs can sequence abstract I/O actions before execution. section 13.5
  • Keep blocking and non-blocking interpreters interchangeable by changing the target monad, while recognizing that ordinary readLine and println remain blocking. section 13.5

Reading bytes through a callback

A Source exposes readBytes(numBytes, callback). nonblockingRead wraps that operation as Par[Either[Throwable, Array[Byte]]], and readPar lifts it into Free[Par, ...]. A larger program can then sequence chunk1 <- readPar(src, 1024) and chunk2 <- readPar(src, 1024) without directly managing the callback protocol. section 13.5

trait Source:
  def readBytes(
    numBytes: Int,
    callback: Either[Throwable, Array[Byte]] => Unit): Unit

Inspect the callback-based Source interface that defines non-blocking byte acquisition. section 13.5

def nonblockingRead(source: Source, numBytes: Int):
    Par[Either[Throwable, Array[Byte]]] =
  async: (cb: Either[Throwable, Array[Byte]] => Unit) =>
    source.readBytes(numBytes, cb)
 
def readPar(source: Source, numBytes: Int):
    Free[Par, Either[Throwable, Array[Byte]]] =
  Suspend(nonblockingRead(source, numBytes))

Inspect the adapter that turns Source.readBytes into a composable Par action. section 13.5

How Functional Programming in Scala, Second Edition covers it

How deep it goes
Explains callback representations, monadic adaptation, and interpreter choice, but leaves library-specific asynchronous implementation details abstract.
Examples
Uses callback-based byte sources, nonblockingRead, sequenced 1024-byte reads, and a Console program converted into Par.
What it assumes
Assumes familiarity with callbacks and effect representations while defining the adaptation from Source operations to Par and Free.
What you can do after
Build it from scratch by adapting callback-based byte operations into Par and sequencing them through Free.
How it connects
Connects non-blocking I/O with callbacks, continuations, Future, Par, Free, monads, and asynchronous I/O libraries.

Also explained in

Compare how 5 books explain non-blocking I/O →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Functional Programming in Scala, Second Edition. The section links point to the parts of the book it draws on.