Search code examples
scalafuturestandard-library

What about a Scala Future#collectWith method?


There're map/flatMap methods, there're also recover/recoverWith methods in the Scala Future standard API.

Why there's no collectWith ?

The code of the collect method is pretty simple :

def collect[S](pf: PartialFunction[T, S])(implicit executor: ExecutionContext): Future[S] =
  map {
    r => pf.applyOrElse(r, (t: T) => throw new NoSuchElementException("Future.collect partial function is not defined at: " + t))
  }

The code of the collectWith method is then easy to imagine :

def collectWith[S](pf: PartialFunction[T, Future[S]])(implicit executor: ExecutionContext): Future[S] =
  flatMap {
    r => pf.applyOrElse(r, (t: T) => throw new NoSuchElementException("Future.collect partial function is not defined at: " + t))
  }

I know that I can implement it and "extend" the Future standard API easily thanks to this article : http://debasishg.blogspot.fr/2008/02/why-i-like-scalas-lexically-scoped-open.html

I done that in my project :

class RichFuture[T](future: Future[T]) {
  def collectWith[S](pf: PartialFunction[T, Future[S]])(implicit executor: ExecutionContext): Future[S] =
    future.flatMap {
      r => pf.applyOrElse(r, (t: T) => throw new NoSuchElementException("Future.collect partial function is not defined at: " + t))
    }
}

trait WithRichFuture {
  implicit def enrichFuture[T](person: Future[T]): RichFuture[T] = new RichFuture(person)
}

Maybe my needs for that does not justify to implement it in the standard API ?

Here is why I need this method in my Play2 project :

def createCar(key: String, eligibleCars: List[Car]): Future[Car] = {
  def handleResponse: PartialFunction[WSResponse, Future[Car]] = {
    case response: WSResponse if response.status == Status.CREATED => Future.successful(response.json.as[Car])
    case response: WSResponse
        if response.status == Status.BAD_REQUEST && response.json.as[Error].error == "not_the_good_one" =>
          createCar(key, eligibleCars.tail)
  }

  // The "carApiClient.createCar" method just returns the result of the WS API call.
  carApiClient.createCar(key, eligibleCars.head).collectWith(handleResponse)
}

I don't know how to do that without my collectWith method.

Maybe it's not the right way to do something like this ?
Do you know a better way ?


EDIT:

I have maybe a better solution for the createCar method that does not requires the collectWith method :

def createCar(key: String, eligibleCars: List[Car]): Future[Car] = {
  for {
    mayCar: Option[Car] <- Future.successful(eligibleCars.headOption)
    r: WSResponse <- carApiClient.createCar(key, mayCar.get) if mayCar.nonEmpty
    createdCar: Car <- Future.successful(r.json.as[Car]) if r.status == Status.CREATED
    createdCar: Car <- createCar(key, eligibleCars.tail) if r.status == Status.BAD_REQUEST && r.json.as[Error].error == "not_the_good_one"
  } yield createdCar
}

What do you think about this second solution ?


Second edit:

Just for information, here is my final solution thanks to @Dylan answer :

def createCar(key: String, eligibleCars: List[Car]): Future[Car] = {

  def doCall(head: Car, tail: List[Car]): Future[Car] = {
    carApiClient
      .createCar(key, head)
      .flatMap( response =>
        response.status match {
          case Status.CREATED => Future.successful(response.json.as[Car])
          case Status.BAD_REQUEST if response.json.as[Error].error == "not_the_good_one" =>
            createCar(key, tail)
        }
      )
  }

  eligibleCars match {
    case head :: tail => doCall(head, tail)
    case Nil => Future.failed(new RuntimeException)
  }

}

Jules


Solution

  • How about:

    def createCar(key: String, eligibleCars: List[Car]): Future[Car] = {
      def handleResponse(response: WSResponse): Future[Car] = response.status match {
        case Status.Created => 
          Future.successful(response.json.as[Car])
        case Status.BAD_REQUEST if response.json.as[Error].error == "not_the_good_one" =>
          createCar(key, eligibleCars.tail)
        case _ =>
          // just fallback to a failed future rather than having a `collectWith`
          Future.failed(new NoSuchElementException("your error here"))
      }
    
      // using flatMap since `handleResponse` is now a regular function
      carApiClient.createCar(key, eligibleCars.head).flatMap(handleResponse)
    }
    

    Two changes:

    • handleResponse is no longer a partial function. The case _ returns a failed future, which is essentially what you were doing in your custom collectWith implementation.
    • use flatMap instead of collectWith, since handleResponse now suits that method signature

    edit for extra info

    If you really need to support the PartialFunction approach, you could always convert a PartialFunction[A, Future[B]] to a Function[A, Future[B]] by calling orElse on the partial function, e.g.

    val handleResponsePF: PartialFunction[WSResponse, Future[Car]] = {
      case ....
    }
    
    val handleResponse: Function[WSResponse, Future[Car]] = handleResponsePF orElse {
      case _ => Future.failed(new NoSucheElementException("default case"))
    }
    

    Doing so would allow you to adapt an existing partial function to fit into a flatMap call.

    (okay, technically, it already does, but you'd be throwing MatchErrors rather than your own custom exceptions)