Scala Future#collectWith 方法怎么样?

What about a Scala Future#collectWith method?

map/flatMap 方法,Scala Future 标准中还有 recover/recoverWith 方法 API。

为什么没有 collectWith

collect 方法的代码非常简单:

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

collectWith方法的代码就很容易想象了:

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

我知道我可以实现它和 "extend" 未来标准 API 很容易感谢这篇文章:http://debasishg.blogspot.fr/2008/02/why-i-like-scalas-lexically-scoped-open.html

我在我的项目中做到了:

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

也许我的需求不足以在标准中实现它 API ?

这就是我在 Play2 项目中需要此方法的原因:

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

如果没有我的 collectWith 方法,我不知道该怎么做。

也许这样做不是正确的方法?
你知道更好的方法吗?


编辑:

对于不需要 collectWith 方法的 createCar 方法,我可能有更好的解决方案:

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
}

您如何看待第二种解决方案?


第二次编辑:

仅供参考,感谢@Dylan 的回答,这是我的最终解决方案:

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

}

朱尔斯

怎么样:

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

两个变化:

  • handleResponse 不再是偏函数。 case _ returns 一个失败的未来,这基本上就是您在自定义 collectWith 实现中所做的。
  • 使用 flatMap 而不是 collectWith,因为 handleResponse 现在适合该方法签名

编辑以获取更多信息

如果你真的需要支持 PartialFunction 方法,你总是可以通过在部分函数上调用 orElsePartialFunction[A, Future[B]] 转换为 Function[A, Future[B]],例如

val handleResponsePF: PartialFunction[WSResponse, Future[Car]] = {
  case ....
}

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

这样做可以让您调整现有的部分函数以适应 flatMap 调用。

(好吧,从技术上讲,它已经这样做了,但你会抛出 MatchErrors 而不是你自己的自定义异常)