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
Eitherrepresents a value or an error;Tryevaluates 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
flatMapdoes not guarantee purity, laziness or cancellation. - Scala
Futurecommonly 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.