Map/FlatMap Purity
Both ZIO and Cats Effect guarantee referential transparency through their effect types.
Map (Transform Success)
import cats.effect.IO
// Transform success value
val mapped: IO[Int] =
IO.pure(21)
.map(_ * 2)
.map(_ + 10)
// Result: IO containing 52map - transform success value
import zio._
// Transform success value
val mapped: UIO[Int] =
ZIO.succeed(21)
.map(_ * 2)
.map(_ + 10)
// Result: UIO containing 52map - transform success value
FlatMap (Chain Effects)
import cats.effect.IO
// For-comprehension
val program: IO[Int] = for {
x <- IO.pure(10)
y <- IO.pure(20)
z <- IO.pure(x + y)
} yield z * 2
// Equivalent to nested flatMap
val manual: IO[Int] =
IO.pure(10).flatMap { x =>
IO.pure(20).flatMap { y =>
IO.pure(x + y).map { z =>
z * 2
}
}
}flatMap / for-comprehension
import zio._
// For-comprehension
val program: UIO[Int] = for {
x <- ZIO.succeed(10)
y <- ZIO.succeed(20)
z <- ZIO.succeed(x + y)
} yield z * 2
// Equivalent to nested flatMap
val manual: UIO[Int] =
ZIO.succeed(10).flatMap { x =>
ZIO.succeed(20).flatMap { y =>
ZIO.succeed(x + y).map { z =>
z * 2
}
}
}flatMap / for-comprehension
Error Short-Circuiting
import cats.effect.IO
// Errors short-circuit the chain
val shortCircuit: IO[Int] = for {
_ <- IO.raiseError[Unit](new Exception("fail"))
_ <- IO.println("never runs")
x <- IO.pure(42)
} yield x
// Result: Exception("fail")Errors propagate, skipping subsequent steps
import zio._
// Errors short-circuit the chain
val shortCircuit: IO[String, Int] = for {
_ <- ZIO.fail("error")
_ <- ZIO.succeed(println("never runs"))
x <- ZIO.succeed(42)
} yield x
// Result: ZIO.fail("error")Errors propagate, skipping subsequent steps
Both libraries short-circuit on errors in flatMap chains. This is called "fail-fast" semantics and is fundamental to monadic error handling.
Utility Operators
Both libraries provide utility operators that simplify common patterns.
Discard Value (as/unit)
import cats.effect.IO
// Map to constant value
val answer: IO[String] =
IO.pure(42).as("answer")
// Discard value to Unit
val unit: IO[Unit] =
IO.pure(42).void.as / .void - constant or unit value
import zio._
// Map to constant value
val answer: UIO[String] =
ZIO.succeed(42).as("answer")
// Discard value to Unit
val unit: UIO[Unit] =
ZIO.succeed(42).unit.as / .unit - constant or unit value
Side Effects (tap)
import cats.effect.IO
import cats.syntax.all._
// Execute side effect, pass through value
val tapped: IO[Int] =
IO.pure(42)
.flatTap(n => IO.println(s"Got: $$n")).flatTap - side effect, keep value
import zio._
// Execute side effect, pass through value
val tapped: UIO[Int] =
ZIO.succeed(42)
.tap(n => ZIO.succeed(println(s"Got: $$n"))).tap - side effect, keep value
.tap is useful for logging, debugging, or metrics without breaking the effect chain. The side effect runs, but the original value passes through unchanged.
Filter or Fail
import cats.effect.IO
import cats.syntax.all._
// Filter or raise error
val filtered: IO[Int] =
IO.pure(42)
.ensure(new Exception("Value too small"))(_ > 50).raiseErrorUnless / .raiseErrorWhen
import zio._
// Filter or fail with custom error
val filtered: IO[String, Int] =
ZIO.succeed(42)
.filterOrFail(_ > 50)("Value too small").filterOrFail - filter or fail
filterOrFail fails when the predicate returns false. Use .filter (on Option-returning effects) to return None instead of failing.
Parallel Composition
import cats.effect.IO
import cats.syntax.all._
// Parallel tuple
val parallel: IO[(Int, String)] =
(IO.pure(42), IO.pure("hello")).parTupled
// Parallel map
val parMapped: IO[Int] =
(IO.pure(1), IO.pure(2), IO.pure(3))
.parMapN(_ + _ + _)parTupled / parMapN - parallel execution
import zio._
// Parallel zip
val parallel: UIO[(Int, String)] =
ZIO.succeed(42) <&> ZIO.succeed("hello")
// Parallel map
val parMapped: UIO[Int] =
ZIO.succeed(1)
.zipWithPar(ZIO.succeed(2))(_ + _)
.zipWithPar(ZIO.succeed(3))(_ + _)<&> / zipWithPar - parallel execution
Sequencing Collections
import cats.effect.IO
import cats.syntax.all._
// Sequential
val sequential: IO[List[Int]] =
List(IO.pure(1), IO.pure(2), IO.pure(3))
.sequence
// Parallel
val parallel: IO[List[Int]] =
List(IO.pure(1), IO.pure(2), IO.pure(3))
.parSequencesequence / parSequence
import zio._
// Sequential
val sequential: UIO[List[Int]] =
ZIO.collectAll(
List(ZIO.succeed(1), ZIO.succeed(2), ZIO.succeed(3))
)
// Parallel
val parallel: UIO[List[Int]] =
ZIO.collectAllPar(
List(ZIO.succeed(1), ZIO.succeed(2), ZIO.succeed(3))
)collectAll / collectAllPar
Traverse
import cats.effect.IO
import cats.syntax.all._
// Traverse with effect
val traversed: IO[List[Int]] =
List(1, 2, 3).traverse(n => IO.pure(n * 2))
// Parallel traverse
val parTraversed: IO[List[Int]] =
List(1, 2, 3).parTraverse(n => IO.pure(n * 2))traverse / parTraverse
import zio._
// Traverse with effect
val traversed: UIO[List[Int]] =
ZIO.foreach(List(1, 2, 3))(n => ZIO.succeed(n * 2))
// Parallel traverse
val parTraversed: UIO[List[Int]] =
ZIO.foreachPar(List(1, 2, 3))(n => ZIO.succeed(n * 2))foreach / foreachPar
Next Steps
With functional composition covered, let's explore dependency injection patterns.