Search code examples
scalayieldfor-comprehension

Return a specific type from for yield in Scala


I am trying to use a function to call 3 separate APIs asynchronously and collate the responses into a UserDetails object and return this as part of the function.

However, the issue I am experiencing is that when I explicitly set the return type to Future[UserDetails] it does not compile as the return from the yield is not a UserDetails object and I can't work out how to return what I want from this.

Note I am looking to return a Future[UserDetails] but the inferred return type is Future[Object]

My code is currently as below:

def getUserDetails(userId: String) = {
  usersConnector.getUserById(userId).map {
    case Some(user) =>
      for {
        connections <- connectionsConnector.getAllConnections(user.username) recover {case _ => None}
        pendingConnections <- connectionsConnector.getAllPendingConnections(user.username) recover {case _ => None}
        userLocation <- userLocationsConnector.getLocationByUsername(user.username) recover {case _ => None}
      } yield {
        UserDetails(Some(user), connections, pendingConnections, userLocation)
      }
    case None => UserDetails(None, None, None, None)
  }
}

Solution

  • The problem is that the cases in your map function do not return the same type. The None case returns UserDetails. However, the Some case returns a Future[UserDetails]. The least upper bound (least common ancestor) of these two types is AnyRef, that is, Object. As a result, the map function transforms the Future[Option[User]] into a Future[Object].

    There are two ways to resolve this. First is two use the Future.successful constructor in the None case:

    ...
    case None => Future.successful(UserDetails(None, None, None, None))
    ...
    

    Now, the resulting future has the type Future[Future[UserDetails]]. You only need to flatten it at this point:

    ...
    }.flatMap(x => x) // add at the end
    

    The second, more elegant way is to embed everything into the for-comprehension:

    def getUserDetails(userId: String) = {
      def detailsFor(user: Option[User]) = user match {
        case Some(user) =>
          for {
            connections <- connectionsConnector.getAllConnections(user.username) recover {case _ => None}
            pendingConnections <- connectionsConnector.getAllPendingConnections(user.username) recover {case _ => None}
            userLocation <- userLocationsConnector.getLocationByUsername(user.username) recover {case _ => None}
          } yield {
            UserDetails(Some(user), connections, pendingConnections, userLocation)
          }
        case None => Promise.successful(UserDetails(None, None, None, None))
      }
      for {
        user <- usersConnector.getUserById(userId)
        userDetails <- detailsFor(user)
      } yield userDetails
    }
    

    EDIT:

    See Łukasz's comment below about starting futures asynchronously in a for-comprehension.