(2) 📦 Monads

From the box analogy to composition

A box is a useful first picture for Option[A], but it is not the definition of a monad. The useful idea is sequencing computations that return values in a context: absence for Option, alternatives for List, or a described effect for an effect type.

map applies A => B inside a context. flatMap connects a value in F[A] to a next step A => F[B], avoiding an extra nested context. pure embeds a plain value. A lawful implementation must also obey the identity and associativity laws below.

Option: dependent steps with possible absence

def half(n: Int): Option[Int] =
  if (n % 2 == 0) Some(n / 2) else None

val mapped = Some(8).map(half)       // Some(Some(4))
val chained = Some(8).flatMap(half)  // Some(4)
val result = half(8).flatMap(half)   // Some(2)
val absent = half(6).flatMap(half)   // None

None skips the remaining dependent step. That models absence; it does not catch arbitrary exceptions, defer I/O or make an impure callback pure. At a nullable boundary, Option(nullableValue) converts null to None; arbitrary Some(null) is still possible.

For comprehensions

val result = for {
  a <- half(8)
  b <- half(a)
} yield b
// Equivalent here to half(8).flatMap(a => half(a).map(b => b))

A comprehension is syntax over the available methods, not a separate execution engine. Evaluation, failure and concurrency behavior come from the receiver type.

The laws

For compatible types, a lawful pure and flatMap satisfy:

pure(a).flatMap(f) == f(a)                       // left identity
m.flatMap(pure) == m                            // right identity
m.flatMap(f).flatMap(g) ==
  m.flatMap(a => f(a).flatMap(g))                // associativity

Here == means the relevant notion of equivalent computation, not necessarily object identity. Associativity permits regrouping; it does not permit swapping f and g. Right identity wraps a value with pure, rather than returning the unwrapped value.

Errors and effects: keep the distinctions

  • Either represents a value or an error; Try evaluates a supplied computation and captures non-fatal exceptions. Neither automatically reverses side effects already performed.
  • Effect libraries can describe I/O as a value and control when it runs. A type merely exposing flatMap does not guarantee purity, laziness or cancellation.
  • Scala Future commonly starts work when created. Its timing and observable effects need care; it is not interchangeable with a deferred effect description.
  • Arbitrary monads do not compose automatically into another monad. Nested contexts may require explicit sequencing, a transformer or a more suitable abstraction.

Takeaway

Ask what the context means, how the next step is sequenced, and which laws/behaviors the implementation actually provides. The box analogy helps with Option, but the composition contract does the real work.

References: Cats Monad documentation; Scala for comprehensions; Scala Futures.

Leave a Reply

Your email address will not be published. Required fields are marked *