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:

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))
// )