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 bothFunctors, thenNested[F, G, *]is also aFunctor(we saw this in action in the example above) - If
F[_]andG[_]are bothApplicatives, thenNested[F, G, *]is also anApplicative - If
F[_]is anApplicativeErrorandG[_]is anApplicative, thenNested[F, G, *]is anApplicativeError - If
F[_]andG[_]are bothTraverses, 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 UserInfos,
creates a list of Users:
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))
// )