Nested
API Documentation: Nested
Motivation
In day-to-day programming we quite often end up with data inside nested
effects, e.g. an integer inside an Either
, which in turn is nested inside
an Option
:
import cats.data.Validated
import cats.data.Validated.Valid
val x: Option[Validated[String, Int]] = Some(Valid(123))
This can be quite annoying to work with, as you have to traverse the nested
structure every time you want to perform a map
or something similar:
x.map(_.map(_.toString))
// res0: Option[Validated[String, String]] = Some(value = Valid(a = "123"))
Nested
can help with this by composing the two map
operations into one:
import cats.data.Nested
import cats.syntax.all._
val nested: Nested[Option, Validated[String, *], Int] = Nested(Some(Valid(123)))
nested.map(_.toString).value
// res1: Option[Validated[String, String]] = Some(value = Valid(a = "123"))
In a sense, Nested
is similar to monad transformers like OptionT
and
EitherT
, as it represents the nesting of effects inside each other. But
Nested
is more general - it does not place any restriction on the type of the
two nested effects:
final case class Nested[F[_], G[_], A](value: F[G[A]])
Instead, it provides a set of inference rules based on the properties of F[_]
and G[_]
. For example:
- If
F[_]
andG[_]
are bothFunctor
s, thenNested[F, G, *]
is also aFunctor
(we saw this in action in the example above) - If
F[_]
andG[_]
are bothApplicative
s, thenNested[F, G, *]
is also anApplicative
- If
F[_]
is anApplicativeError
andG[_]
is anApplicative
, thenNested[F, G, *]
is anApplicativeError
- If
F[_]
andG[_]
are bothTraverse
s, thenNested[F, G, *]
is also aTraverse
You can see the full list of these rules in the Nested
companion object.
A more interesting example
(courtesy of Channing Walton and Luka Jacobowitz via Twitter, slightly adapted)
Say we have an API for creating users:
import scala.concurrent.Future
case class UserInfo(name: String, age: Int)
case class User(id: String, name: String, age: Int)
def createUser(userInfo: UserInfo): Future[Either[List[String], User]] =
Future.successful(Right(User("user 123", userInfo.name, userInfo.age)))
Using Nested
we can write a function that, given a list of UserInfo
s,
creates a list of User
s:
import scala.concurrent.Await
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
import cats.Applicative
import cats.data.Nested
import cats.syntax.all._
def createUsers(userInfos: List[UserInfo]): Future[Either[List[String], List[User]]] =
userInfos.traverse(userInfo => Nested(createUser(userInfo))).value
val userInfos = List(
UserInfo("Alice", 42),
UserInfo("Bob", 99)
)
Await.result(createUsers(userInfos), 1.second)
// res2: Either[List[String], List[User]] = Right(
// value = List(
// User(id = "user 123", name = "Alice", age = 42),
// User(id = "user 123", name = "Bob", age = 99)
// )
// )
Note that if we hadn't used Nested
, the behaviour of our function would have
been different, resulting in a different return type:
def createUsersNotNested(userInfos: List[UserInfo]): Future[List[Either[List[String], User]]] =
userInfos.traverse(createUser)
Await.result(createUsersNotNested(userInfos), 1.second)
// res3: List[Either[List[String], User]] = List(
// Right(value = User(id = "user 123", name = "Alice", age = 42)),
// Right(value = User(id = "user 123", name = "Bob", age = 99))
// )