3

I am building a reactive site in Scala and Play Framework, and my data models are such that I often need to compose Future and Option, and build Future of List / Set from previous values to get the result I need.

I wrote a simple app with a fake data source that you can copy and paste and it should compile. My question is, how can I get the result back, in my case UserContext, in a consumable form. Currently, I am getting back Future[Option[Future[UserContext]]].

I want to do this in pure Scala to learn the language better, so I am avoiding Scalaz at the moment. Although I know I should eventually use that.

package futures

import scala.concurrent.{Future, ExecutionContext}

// http://www.edofic.com/posts/2014-03-07-practical-future-option.html
case class FutureO[+A](future: Future[Option[A]]) extends AnyVal {

  def flatMap[B](f: A => FutureO[B])(implicit ec: ExecutionContext): FutureO[B] = {
    FutureO {
      future.flatMap { optA =>
        optA.map { a =>
          f(a).future
        } getOrElse Future.successful(None)
      }
    }
  }

  def map[B](f: A => B)(implicit ec: ExecutionContext): FutureO[B] = {
    FutureO(future.map(_ map f))
  }
}

// ========== USAGE OF FutureO BELOW ============= \\

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

object TeamDB {

  val basketballTeam = Team(id = 111, player_ids = Set(111, 222))
  val baseballTeam = Team(id = 222, player_ids = Set(333))

  def findById(teamId: Int): Future[Option[Team]] = Future.successful(
    teamId match {
      case 111 => Some(basketballTeam)
      case 222 => Some(baseballTeam)
      case _ => None
    }
  )
}

object PlayerDB {

  val basketballPlayer1 = Player(id = 111, jerseyNumber = 23)
  val basketballPlayer2 = Player(id = 222, jerseyNumber = 45)
  val baseballPlayer = Player(id = 333, jerseyNumber = 5)

  def findById(playerId: Int): Future[Option[Player]] = Future.successful(
    playerId match {
      case 111 => Some(basketballPlayer1)
      case 222 => Some(basketballPlayer2)
      case 333 => Some(baseballPlayer)
      case _ => None
    }
  )
}

object UserDB {

  // user1 is on BOTH the baseball and basketball team
  val user1 = User(id = 111, name = "Michael Jordan", player_ids = Set(111, 333), team_ids = Set(111, 222))

  // user2 is ONLY on the basketball team
  val user2 = User(id = 222, name = "David Wright", player_ids = Set(222), team_ids = Set(111))

  def findById(userId: Long): Future[Option[User]] = Future.successful(
    userId match {
      case 111 => Some(user1)
      case 222 => Some(user2)
      case _ => None
    }
  )
}

case class User(id: Int, name: String, player_ids: Set[Int], team_ids: Set[Int])
case class Player(id: Int, jerseyNumber: Int)
case class Team(id: Int, player_ids: Set[Int])
case class UserContext(user: User, teams: Set[Team], players: Set[Player])

object FutureOptionListTest extends App {

  val result = for {
    user <- FutureO(UserDB.findById(userId = 111))

  } yield for {
      players: Set[Option[Player]] <- Future.traverse(user.player_ids)(x => PlayerDB.findById(x))
      teams: Set[Option[Team]] <- Future.traverse(user.team_ids)(x => TeamDB.findById(x))

    } yield {
        UserContext(user, teams.flatten, players.flatten)

      }

  result.future // returns Future[Option[Future[UserContext]]] but I just want Future[UserContext] or UserContext
}
Peter Neyens
  • 9,770
  • 27
  • 33

2 Answers2

6

You have created FutureO which combines the effects of Future and Option (if you are looking into Scalaz this compares with OptionT[Future, ?]).

Remembering that for ... yield is analogous to FutureO.map, the result type will always be FutureO[?] (and Future[Option[?]] if you do result.future).

The problem is you want to return a Future[UserContex] instead of a Future[Option[UserContext]]. Essentially you want to loose the Option context, so somewhere you need to explicitly handle if the user exists or not.

A possible solution in this case could be to leave out the FutureO since you are only using it once.

case class NoUserFoundException(id: Long) extends Exception 

// for comprehension with Future
val result = for {
  user <- UserDB.findById(userId = 111) flatMap (
            // handle Option (Future[Option[User]] => Future[User])
            _.map(user => Future.successful(user))
             .getOrElse(Future.failed(NoUserFoundException(111)))
          )
  players <- Future.traverse(user.player_ids)(x => PlayerDB.findById(x))
  teams  <- Future.traverse(user.team_ids)(x => TeamDB.findById(x))
} yield UserContext(user, teams.flatten, players.flatten)
// result: scala.concurrent.Future[UserContext]

If you had multiple functions returning a Future[Option[?]], you probably would like to use FutureO, in this case you could create an extra function Future[A] => FutureO[A], so you can use your functions in the same for comprehension (all in the FutureO monad):

def liftFO[A](fut: Future[A]) = FutureO(fut.map(Some(_)))

// for comprehension with FutureO
val futureO = for {
  user <- FutureO(UserDB.findById(userId = 111))
  players <- liftFO(Future.traverse(user.player_ids)(x => PlayerDB.findById(x)))
  teams  <- liftFO(Future.traverse(user.team_ids)(x => TeamDB.findById(x)))
} yield UserContext(user, teams.flatten, players.flatten)
// futureO: FutureO[UserContext]

val result = futureO.future flatMap (
   // handle Option (Future[Option[UserContext]] => Future[UserContext])
   _.map(user => Future.successful(user))
    .getOrElse(Future.failed(new RuntimeException("Could not find UserContext")))
)
// result: scala.concurrent.Future[UserContext]

But as you can see, you will always need to handle the "option context" before you can return a Future[UserContext].

Peter Neyens
  • 9,770
  • 27
  • 33
  • Thank you Peter! I like the second approach of normalizing everything to FutureO. I actually do have more use cases that involve multiple FutureO, so this solution will work perfectly. Thank you for thinking ahead and suggesting this! – sanchezjjose Oct 06 '15 at 00:14
2

To expand on Peter Neyens' answer, often I'll put a bunch of monad -> monad transformations in a special implicit class and import them as I need them. Here we have two monads, Option[T] and Future[T]. In this case, you are treating None as being a failed Future. You could probably do this:

package foo {
    class OptionOps[T](in: Option[T]) {
        def toFuture: Future[T] = in match {
            case Some(t) => Future.successful(t)
            case None => Future.failed(new Exception("option was none"))
        }
    }
    implicit def optionOps[T](in: Option[T]) = new OptionOps[T](in)
}

Then you just import it import foo.optionOps

And then:

val a: Future[Any] = ...
val b: Option[Any] = Some("hi")
for {
    aFuture <- a
    bFuture <- b.toFuture
} yield bFuture // yields a successful future containing "hi"
Daniel Fithian
  • 101
  • 1
  • 5
  • Awesome thanks for your suggestion Daniel! Making the monad transformations implicit and grouping into a class is a nice improvement. – sanchezjjose Oct 12 '15 at 22:21
  • Is there possibly a syntax error here? I'm a bit confused as to which scope the `implicit def optionOps[T]...` belongs to and how this ties to @peter-neyens answer. Apologies Scala newbie here. – janakagamini Apr 20 '16 at 03:56
  • Importing `foo.optionOps` into a package will expose `toFuture` to any `Option[T]`. `b` is `Option[Any]`, so `b.toFuture` yields `Future[Any]`. It's just a different solution to flattening `Future[Option[Future[T]]]` by converting the option to a future. – Daniel Fithian Apr 27 '16 at 15:08