Overview

12 Functional input/output

This chapter moves from building purely functional computations to producing observable effects safely. It clarifies the difference between side effects (incidental to a function’s return value) and effects (the intended observable outcome), and shows how to preserve referential transparency by applying effects inside computational contexts rather than extracting values. Using contexts such as Result, Option, List, Lazy, and Stream, functions are composed safely and effects are injected with dedicated combinators, notably forEach in Result to handle success, failure, and empty cases, enabling tasks like console output without breaking functional discipline.

To bring data into programs, the chapter introduces an Input abstraction that returns Result> so that reads can be chained transparently. A shared AbstractReader captures common logic, with ConsoleReader for interactive prompts and FileReader for file-based input, including proper resource handling. Because input is modeled as values in context, it is easy to assemble end-to-end flows (prompt, parse, transform, display) and to test them deterministically via a ScriptReader that feeds scripted inputs, keeping the core logic clean and testable.

For fully functional input/output, effects are represented as programs—values of an IO type—that are composed now and executed later. The chapter evolves IO from a simple thunk to a parameterized IO, adds map and flatMap, and introduces ways to combine programs (plus, sequence, map2) and build control structures (repeat, condition, doWhile) in a functional DSL. It also tackles stack safety: naive composition can overflow the stack, so a trampolined, sealed IO with Return, Suspend, and Continue and a tail-recursive interpreter is used to run large or infinite programs safely. The overall guidance is pragmatic: keep computations pure, push effects to the edges, and adopt the IO approach when the extra power outweighs the added complexity.

 Summary

  • Effects can be passed into List, Result, and other contexts to be safely applied to values, rather than extracting values from these contexts and applying the effects outside, which might produce errors if there are no values.
  • Handling two different effects for success and failure can be abstracted inside the Result type.
  • Reading from files is done in exactly the same way as reading from the console or from memory through the Reader abstraction.
  • More functional input/output can be obtained through the IO type.
  • The IO type can be extended to a more generic type that makes it possible to perform any imperative task in a functional way by building a program that will be executed later.
  • The IO type can be made stack-safe by using the same techniques we used for stack-safe recursive functions.

FAQ

What are “effects” and “side effects,” and how can we handle effects in a functional way?An effect is anything observable outside a program (for example, printing to the console). A side effect is anything observable that happens in addition to a function’s return value. We can handle effects functionally by keeping pure computations referentially transparent and modeling effects as values (for example, functions like (T) -> Unit or programs wrapped in IO) that are applied at the boundaries.
How do I apply an effect to a Result without breaking purity?Don’t extract the value; pass the effect into the context. Use Result.forEach(onSuccess, onFailure, onEmpty) to apply effects, or map an effect of type (A) -> Unit and ignore the Result. Example: result.forEach(::println, { e -> println(e.message) })
Why do Input.readString/readInt return Result<Pair<A, Input>> instead of Result<A>?Returning the next Input along with the value preserves referential transparency and enables safe chaining. Each read yields both the parsed value and the updated input source, letting you flatMap subsequent reads without hidden mutable state.
How can I make console/file input deterministic and testable?Abstract input via an Input interface and provide implementations: ConsoleReader (console), FileReader (files, returning Result), and ScriptReader (predefined commands for tests). Compose reads with flatMap, and produce collections with Stream.unfold for predictable, testable programs.
What is the IO type and why introduce it?IO is a type that represents an effectful program as a value. Instead of performing effects, your pure code returns an IO that, when invoked, executes the effects. This cleanly separates pure composition from effect execution and allows combining/transforming programs before running them.
How do I combine IO actions?Add a plus operator or use sequence/folds. For example: val script = io1 + io2 + io3; script() runs them in order. For collections, fold with an identity like IO.empty. For combining results, use map2(ioa, iob) { a -> { b -> f(a, b) } }, and build higher-level combinators such as repeat(n, io).
How do map and flatMap work for IO, and why are they needed?map transforms the result without running effects: fun <B> map(g: (A) -> B): IO<B> = IO { g(this()) }. flatMap sequences dependent effects: fun <B> flatMap(g: (A) -> IO<B>): IO<B> = IO { g(this())() }. They let you compose input/output pipelines without prematurely executing effects.
Can I express loops and conditionals with IO?Yes. Build higher-level combinators in your IO DSL: repeat(n, io) to run an action n times, condition to branch, doWhile to loop based on a predicate, and sequence to run several IOs in order. These are constructed from map/flatMap (and helpers like map2).
Why can naive IO composition blow the stack, and how do I make it stack safe?Chaining many flatMap calls creates deep recursion when the program runs, causing stack overflows (for example, forever(io) loops). Make IO stack-safe by trampolining: represent programs as a sealed hierarchy (Return, Suspend, Continue) and interpret them with a tail-recursive invokeHelper that iteratively steps through the structure.
When should I use fully functional IO in Kotlin?Use it when you need maximal purity, composability, and testability of effectful logic. However, it adds complexity. For many apps, a simpler approach—pure functional core plus small, explicit I/O shell (forEach on Result/List, explicit Input/Output boundaries)—is sufficient and more idiomatic in Kotlin.
choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • The Joy of Kotlin ebook for free
choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • The Joy of Kotlin ebook for free
choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • The Joy of Kotlin ebook for free