Как насчет метода Scala Future#collectWith?

В стандартном API Scala Future есть методы map/flatMap, а также методы recover/recoverWith.

Почему нет 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))
  }

Я знаю, что могу легко реализовать и «расширить» стандартный API Future благодаря этой статье: scoped-open.html" rel="nofollow">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.

Может быть, это неправильный способ сделать что-то подобное?
Вы знаете лучший способ?


ИЗМЕНИТЬ:

Возможно, у меня есть лучшее решение для метода createCar, которое не требует метода collectWith:

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

}

Жюль


person Jules Ivanic    schedule 09.02.2016    source источник


Ответы (1)


Как насчет:

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 _ возвращает неудачное будущее, что, по сути, вы и делали в своей пользовательской реализации collectWith.
  • используйте flatMap вместо collectWith, так как handleResponse теперь подходит для этой сигнатуры метода

изменить для получения дополнительной информации

Если вам действительно нужно поддерживать подход PartialFunction, вы всегда можете преобразовать PartialFunction[A, Future[B]] в Function[A, Future[B]], вызвав orElse в частичной функции, например.

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

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

Это позволит вам адаптировать существующую частичную функцию к вызову flatMap.

(хорошо, технически это уже так, но вы будете генерировать MatchErrors, а не свои собственные исключения)

person Dylan    schedule 09.02.2016
comment
Я нахожу ваше решение flatMap довольно хорошим, потому что оно приятное! Спасибо =) И спасибо за информацию о переводе PartialFunction в Function. Я не знал этого трюка =) - person Jules Ivanic; 09.02.2016