OptionT

API Documentation: OptionT

OptionT[F[_], A] is a light wrapper on an F[Option[A]]. Speaking technically, it is a monad transformer for Option, but you don't need to know what that means for it to be useful. OptionT can be more convenient to work with than using F[Option[A]] directly.

Reduce map boilerplate

Consider the following scenario:

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

val customGreeting: Future[Option[String]] = Future.successful(Some("welcome back, Lola"))

We want to try out various forms of our greetings.

val excitedGreeting: Future[Option[String]] = customGreeting.map(_.map(_ + "!"))

val hasWelcome: Future[Option[String]] = customGreeting.map(_.filter(_.contains("welcome")))

val noWelcome: Future[Option[String]] = customGreeting.map(_.filterNot(_.contains("welcome")))

val withFallback: Future[String] = customGreeting.map(_.getOrElse("hello, there!"))

As you can see, the implementations of all of these variations are very similar. We want to call the Option operation (map, filter, filterNot, getOrElse), but since our Option is wrapped in a Future, we first need to map over the Future.

OptionT can help remove some of this boilerplate. It exposes methods that look like those on Option, but it handles the outer map call on the Future so we don't have to:

import cats.data.OptionT
import cats.syntax.all._

val customGreetingT: OptionT[Future, String] = OptionT(customGreeting)

val excitedGreeting: OptionT[Future, String] = customGreetingT.map(_ + "!")

val withWelcome: OptionT[Future, String] = customGreetingT.filter(_.contains("welcome"))

val noWelcome: OptionT[Future, String] = customGreetingT.filterNot(_.contains("welcome"))

val withFallback: Future[String] = customGreetingT.getOrElse("hello, there!")

From Option[A] and/or F[A] to OptionT[F, A]

Sometimes you may have an Option[A] and/or F[A] and want to lift them into an OptionT[F, A]. For this purpose OptionT exposes two useful methods, namely fromOption and liftF, respectively. E.g.:

val greetingFO: Future[Option[String]] = Future.successful(Some("Hello"))

val firstnameF: Future[String] = Future.successful("Jane")

val lastnameO: Option[String] = Some("Doe")

val ot: OptionT[Future, String] = for {
  g <- OptionT(greetingFO)
  f <- OptionT.liftF(firstnameF)
  l <- OptionT.fromOption[Future](lastnameO)
} yield s"$g $f $l"

val result: Future[Option[String]] = ot.value // Future(Some("Hello Jane Doe"))

From A to OptionT[F,A]

If you have only an A and you wish to lift it into an OptionT[F,A] assuming you have an Applicative instance for F you can use some which is an alias for pure. There also exists a none method which can be used to create an OptionT[F,A], where the Option wrapped A type is actually a None:

val greet: OptionT[Future,String] = OptionT.pure("Hola!")

val greetAlt: OptionT[Future,String] = OptionT.some("Hi!")

val failedGreet: OptionT[Future,String] = OptionT.none

Beyond map

Sometimes the operation you want to perform on an Future[Option[String]] might not be as simple as just wrapping the Option method in a Future.map call. For example, what if we want to greet the customer with their custom greeting if it exists but otherwise fall back to a default Future[String] greeting? Without OptionT, this implementation might look like:

val defaultGreeting: Future[String] = Future.successful("hello, there")

val greeting: Future[String] = customGreeting.flatMap(custom =>
  custom.map(Future.successful).getOrElse(defaultGreeting))

We can't quite turn to the getOrElse method on OptionT, because it takes a default value of type A instead of Future[A]. However, the getOrElseF method is exactly what we want:

val greeting: Future[String] = customGreetingT.getOrElseF(defaultGreeting)

Getting to the underlying instance

If you want to get the F[Option[A]] value (in this case Future[Option[String]]) out of an OptionT instance, you can simply call value:

val customGreeting: Future[Option[String]] = customGreetingT.value