unsafeRun interprets an IO value: it performs the described external effects and returns the computation’s result. Building an IO remains pure until this boundary is crossed, keeping composition separate from console or other environmental manipulation. section 13.2 section 13.2.1
Its role changes as the IO representation becomes more expressive:
- A basic IO exposes unsafeRun: Unit, so it can execute actions such as PrintLine but cannot return meaningful input. section 13.2
- A parameterized IO[A] returns an A, allowing ReadLine, parsing, sequencing, mapping, and flatMapping while preserving delayed effects. section 13.2.1
- A direct recursive interpreter can overflow the stack when flatMap nests indefinitely, as with a forever-running print action. section 13.3
- A constructor-based IO represents Return, Suspend, and FlatMap explicitly, letting unsafeRun interpret the structure with a tail-recursive loop. section 13.3.1
Executing a composed computation
A converter can return an IO[Unit] that prints a prompt, reads a line, converts it, and prints the result. Calling converter.unsafeRun performs those effects; constructing converter does not. section 13.2.1
@annotation.tailrec final def unsafeRun(): A = this match
case Return(a) => a
case Suspend(r) => r()
case FlatMap(x, f) => x match #1
case Return(a) => f(a).unsafeRun()
case Suspend(r) => f(r()).unsafeRun() #2
case FlatMap(y, g) =>
y.flatMap(a => g(a).flatMap(f)).unsafeRun() #3
Inspect the tail-recursive unsafeRun interpreter and its handling of Return, Suspend, and nested FlatMap cases. section 13.3.1
