6 Dealing with optional data
This chapter explains why representing the absence of data is tricky in programs and how Kotlin encourages safer practices than relying on null pointers. It reviews the pitfalls of “business nulls” and sentinel values that silently blur intent and cause fragile code, then contrasts them with Kotlin’s nullable and non-nullable type system, which forces programmers to acknowledge and handle absence explicitly. The goal is to work with optional data in a way that avoids unchecked nulls, improves readability, and enables functional composition without scattering defensive checks.
To achieve this, the chapter develops an Option type with two cases: Some for the presence of a value and None for its absence. Building on this foundation, it introduces a small, composable toolkit: getOrElse (with eager and lazy defaults), map for transforming contained values, flatMap for chaining computations that may produce options, filter for conditional retention, and orElse for fallback options. It shows how to lift ordinary functions to operate over Option, combine multiple options with map2/map3, and adapt legacy APIs (including those that may throw exceptions). Practical examples include replacing null-returning map lookups with a getOption extension and calculating statistics like variance by composing operations that can short-circuit on missing input.
The chapter also demonstrates composing Option with List via sequence and traverse to invert List
Figure 6.1. Using an Option type for optional data allows you to compose functions even when the data is absent. Without the Option type, composing functions wouldn’t produce a function because the resulting program would potentially throw a NullPointerException.
Figure 6.2. Lifting a function
Summary
- You need a way to represent optional data, which means data that may or may not be present. The
nullpointer is the most impractical and dangerous way to represent the absence of data. Sentinel values and empty lists are other possible ways to represent the absence of data, but they don‘t compose well.
- Kotlin uses nullable types to represent optional data. It protects you against
NullPointerExceptionwhen you use nonnullable types, and it forces you to handlenullwhen using nullable types.
- The
Optiondata type is a much better way to represent optional data. TheSomesubtype represents data, and theNonesubtype represents the absence of data.
- You can apply functions to
Optionthrough themapandflatMaphigher-order functions, allowing for easyOptioncomposition.
- Functions operating on values can be lifted to operate on
Optioninstances.
-
Listcan be composed withOption. AList<Option>can be turned into anOption<List>using thesequencefunction.
-
Optioninstances can be compared for equality. Instances of subtypeSomeare equal if their wrapped values are equal. Because there‘s only one instance ofNone, all instances ofNoneare equal.
- Although
Optionmay represent the result of a computation producing an exception, all information about the occurring exception is lost. In the next chapter, you‘ll learn how to deal with this problem.
FAQ
Why are null references called the “billion dollar mistake” and what problems do they cause?
Null references let “absence” flow through programs invisibly. They lead to NullPointerExceptions at use sites, force pervasive null checks, encourage “business nulls” and sentinel values that blur intent, and can silently mask earlier bugs (for example, passing null to APIs that accept it). They also make function composition brittle because any step can blow up when a null appears.How does Kotlin’s type system help with null safety?
Kotlin distinguishes non-nullable types (e.g., String) from nullable types (String?). You can only assign null to nullable types, and the compiler forces you to handle nulls with safe calls (?.), the Elvis operator (?:), etc. This pushes handling to the edges and prevents many accidental NullPointerExceptions, even though nulls can still arrive from libraries.Why not use sentinel values or 0/1-element lists for optional results?
Sentinel values overload meaning (for example, 0 or Double.NaN might be a real value), don’t generalize across types, and hide intent. Using lists with 0 or 1 element has no type-level guarantee against multiple elements and brings unnecessary list operations. Both approaches are error-prone and harm composability.What is the Option type introduced in this chapter?
Option is a sealed class representing optional data with two cases: Some(value) and None. It’s like a list with at most one element. A companion builder converts nullable values to Option: Option(a) yields None if a is null, otherwise Some(a). This creates a safe context for absence without using null.How do I safely extract a value from Option?
- Eager default: getOrElse(defaultValue) returns the contained value or the provided default. - Lazy default: getOrElse { computeDefault() } defers evaluation so the default is computed only for None. Example:val x = max(list).getOrElse { expensiveFallback() }
Use the lazy version to avoid unnecessary work or side effects when the Option is Some.How do I transform and compose Option values?
- map(f): Option<A> → Option<B> applies f to the value if present. - flatMap(f): Option<A> → Option<B> where f: A → Option<B>, to chain computations that may fail. - filter(p): keeps the value only if it satisfies p, else None. - orElse { other }: provides a fallback Option lazily. These support safe, declarative composition without manual null checks.How can I adapt existing APIs (that return nulls or throw) to Option?
- Wrap nullable results: define extensions like Map.getOption(key) = Option(this[key]). - Lift pure functions: lift(f): (Option<A>) → Option<B> using map. - Lift exception-throwing functions: wrap in try/catch and return None on failure. - hLift: convert f: A → B into A → Option<B> to keep failure handling explicit.How do I combine multiple Option values (e.g., two or three inputs)?
Use map2/map3 with curried functions:fun <A,B,C> map2(oa: Option<A>, ob: Option<B>,
f: (A) -> (B) -> C): Option<C> =
oa.flatMap { a -> ob.map { b -> f(a)(b) } }
This pattern scales (swap the final map for flatMap if f returns Option). It lets you apply ordinary multi-arg functions to optional inputs safely.
The Joy of Kotlin ebook for free