IO represents an interaction with the outside world as a value rather than performing it while ordinary program logic is being evaluated. A pure function can therefore compute a description of reading input, printing output, accessing a database, or writing a file; a separate interpreter performs the described effects. This separates effectful boundaries from a referentially transparent core. section 13.0 section 13.10
Describe a console action first
A contest program can compute the winner and its message purely, then return PrintLine(message): IO. Creating that value does not print anything. Calling unsafeRun interprets it and manipulates the console. The distinction makes the program’s responsibilities explicit: winner and winnerMsg calculate, while IO marks the requested external action. section 13.2
trait IO: def unsafeRun: Unit def PrintLine(msg: String): IO = new: def unsafeRun = println(msg) def contest(p1: Player, p2: Player): IO = PrintLine(winnerMsg(winner(p1, p2)))
The listing contrasts an IO-producing contest function with the single unsafeRun operation that actually invokes println. section 13.2
Output-only IO soon encounters a practical limitation: an action such as readLine produces a value needed by the next action. Parameterizing the type as IO[A] lets execution yield an A. map transforms that eventual result, and flatMap chooses a following IO computation from it. These operations make IO a Monad, so dependent console interactions can be sequenced in a for-comprehension without making the definition itself effectful. section 13.2.1
Temperature converter
The converter first describes a prompt, then reads a Fahrenheit value, converts it with a pure function, and describes printing the Celsius result. The whole sequence has type IO[Unit]. It becomes an external interaction only when unsafeRun is invoked, while the converter value remains reusable and referentially transparent. section 13.2.1
def ReadLine: IO[String] = IO(readLine())
def PrintLine(msg: String): IO[Unit] = IO(println(msg))
def converter: IO[Unit] = for
_ <- PrintLine("Enter a temperature in degrees Fahrenheit: ")
d <- ReadLine.map(_.toDouble)
_ <- PrintLine(fahrenheitToCelsius(d).toString)
yield ()
The converter shows ReadLine, map, PrintLine, and a for-comprehension combined into one IO[Unit] description. section 13.2.1
Because IO computations are ordinary values, they can be stored in collections, passed to functions, assembled dynamically, and packaged as reusable patterns. Separating an interpreter from client code also permits a different interpretation strategy without changing the descriptions. This is language design in miniature: the data type and its operations define an embedded language for effectful programs. section 13.0 section 13.2.2
The simplest implementation, essentially a delayed computation with unsafeRun, has important limits. Large compositions can overflow the runtime stack, and an IO[A] value is opaque: it cannot be inspected to determine what it will do. It also lacks concurrency support. Delaying execution alone is useful, but it does not automatically make imperative-style programs composable. section 13.10 section 13.2.2
A stack-safe representation reifies control flow as data. Return records a completed result, Suspend delays an action, and FlatMap records a continuation. Rather than recursively executing nested flatMaps through ordinary calls, a tail-recursive interpreter traverses these constructors. The same structure is a trampoline for pure computations as well as I/O, trading some direct-call speed for predictable stack use. section 13.3.1 section 13.3.2
Listing 13.4 Creating a new IO type
enum IO[A]:
case Return(a: A) #1
case Suspend(resume: () => A) #2
case FlatMap[A, B](
sub: IO[A], k: A => IO[B]) extends IO[B] #3
def flatMap[B](f: A => IO[B]): IO[B] =
FlatMap(this, f)
def map[B](f: A => B): IO[B] =
flatMap(a => Return(f(a)))
The constructors make completion, suspension, and continuation explicit data that an interpreter can traverse. section 13.3.1
IO can also sit beneath narrower capability interfaces such as Files, Database, and Console, whose operations constrain what a program may request. A general-purpose representation can combine trampolined sequencing with asynchronous execution. Even then, direct IO is a lowest-common-denominator abstraction: efficient streaming file work tends toward monolithic loops, motivating more composable stream processing abstractions. section 13.7 section 13.8





