Почему Finatra использует flatMap, а не только карту?

Это может быть действительно глупый вопрос, но я пытаюсь понять логику использования #flatMap, а не только #map в этом определении метода в Finatra HttpClient definition:

def executeJson[T: Manifest](request: Request, expectedStatus: Status = Status.Ok): Future[T] = {
  execute(request) flatMap { httpResponse =>
    if (httpResponse.status != expectedStatus) {
      Future.exception(new HttpClientException(httpResponse.status, httpResponse.contentString))
    } else {
      Future(parseMessageBody[T](httpResponse, mapper.reader[T]))
        .transformException { e =>
          new HttpClientException(httpResponse.status, s"${e.getClass.getName} - ${e.getMessage}")
        }
    }
  }
}

Зачем создавать новое будущее, если я могу просто использовать #map и вместо этого иметь что-то вроде:

execute(request) map { httpResponse =>
  if (httpResponse.status != expectedStatus) {
    throw new HttpClientException(httpResponse.status, httpResponse.contentString)
  } else {
    try {
      FinatraObjectMapper.parseResponseBody[T](httpResponse, mapper.reader[T])
    } catch {
      case e => throw new HttpClientException(httpResponse.status, s"${e.getClass.getName} - ${e.getMessage}")
    }
  }
}

Будет ли это чисто стилистическое различие, и использование Future.exception в этом случае просто лучший стиль, в то время как throw выглядит почти как побочный эффект (на самом деле это не так, поскольку он не выходит из контекста Future) или есть что-то еще за этим стоит, например, порядок исполнения и тому подобное?

Вкратце: в чем разница между броском в Future и возвратом Future.exception?


person Stefan Pavikevik    schedule 25.11.2020    source источник
comment
Обратите внимание, что использование Future.exception, вероятно, быстрее, чем создание и перехват исключения. - В идеале вы никогда не должны генерировать исключения самостоятельно, а лучше поднимать ошибочные значения внутри Future с помощью комбинаторов, таких как exception.   -  person Luis Miguel Mejía Suárez    schedule 25.11.2020
comment
@LuisMiguelMejíaSuárez спасибо, у вас есть какие-нибудь указатели / ссылки на то, почему это быстрее?   -  person Stefan Pavikevik    schedule 26.11.2020
comment
Потому что создание и перехват исключения происходит медленно, тогда как простое возвращение значения выполняется так же быстро, как и перенастройка любого другого значения. См.: mattwarren.org/2016/12/20/Why -Исключения-должны-быть-исключительными   -  person Luis Miguel Mejía Suárez    schedule 26.11.2020
comment
@LuisMiguelMejíaSuárez а, это имеет смысл .. поскольку Future должен поймать исключение. спасибо за ссылку   -  person Stefan Pavikevik    schedule 26.11.2020


Ответы (2)


С теоретической точки зрения, если мы уберем часть исключений (они все равно не могут быть аргументированы использованием теории категорий), то эти две операции полностью идентичны, пока выбранная вами конструкция (в вашем случае Твиттер Future) образует действительный монада.

Я не хочу вдаваться в подробности этих концепций, поэтому я просто представлю законы напрямую (используя Scala Future):

import scala.concurrent.ExecutionContext.Implicits.global

// Functor identity law
Future(42).map(x => x) == Future(42)

// Monad left-identity law
val f = (x: Int) => Future(x)
Future(42).flatMap(f) == f(42) 

// combining those two, since every Monad is also a Functor, we get:
Future(42).map(x => x) == Future(42).flatMap(x => Future(x))

// and if we now generalise identity into any function:
Future(42).map(x => x + 20) == Future(42).flatMap(x => Future(x + 20))

Так что да, как вы уже намекнули, эти два подхода идентичны.

Тем не менее, у меня есть три комментария по этому поводу, учитывая, что мы включаем в список исключения:

  1. Будьте осторожны — когда дело доходит до генерации исключений, Scala Future (вероятно, и Twitter) намеренно нарушает закон левой идентичности, чтобы обменять его на дополнительную безопасность.

Пример:

import scala.concurrent.ExecutionContext.Implicits.global

def sneakyFuture = {
  throw new Exception("boom!")
  Future(42)
}

val f1 = Future(42).flatMap(_ => sneakyFuture)
// Future(Failure(java.lang.Exception: boom!))

val f2 = sneakyFuture
// Exception in thread "main" java.lang.Exception: boom!
  1. Как упоминал @randbw, создание исключений не является идиоматичным для FP и нарушает такие принципы, как чистота функций и ссылочная прозрачность значений.

Scala и Twitter Future упрощают создание исключения — пока это происходит в контексте Future, исключение не всплывет, а вместо этого приведет к сбою Future. Однако это не означает, что буквальное использование их в вашем коде должно быть разрешено, потому что это разрушает структуру ваших программ (аналогично тому, как это делают операторы GOTO или операторы break в циклах и т. д.).

Предпочтительная практика состоит в том, чтобы всегда оценивать каждый путь кода как значение, а не разбрасывать бомбы, поэтому лучше использовать flatMap в (неудачный) Future, чем сопоставлять какой-либо код, который бросает бомбу.

  1. Помните о ссылочной прозрачности.

Если вы используете map вместо flatMap и кто-то берет код из карты и извлекает его в функцию, то вам будет безопаснее, если эта функция вернет Future, иначе кто-то может запустить ее вне контекста Future.

Пример:

import scala.concurrent.ExecutionContext.Implicits.global

Future(42).map(x => {
  // this should be done inside a Future
  x + 1
})

Это хорошо. Но после полностью корректного рефакторинга (использующего правило ссылочной прозрачности) ваш код становится таким:

def f(x: Int) =  {
  // this should be done inside a Future
  x + 1
}
Future(42).map(x => f(x))

И вы столкнетесь с проблемами, если кто-то позвонит f напрямую. Гораздо безопаснее обернуть код в Future и на нем flatMap.

Конечно, можно возразить, что даже при использовании flatMap кто-то может вырвать f из .flatMap(x => Future(f(x)), но это маловероятно. С другой стороны, простое выделение логики обработки ответа в отдельную функцию идеально соответствует идее функционального программирования о объединении небольших функций в более крупные, и это, вероятно, произойдет.

person slouc    schedule 25.11.2020
comment
Это имеет смысл, хотя я не был уверен, следует ли Твиттер здесь принципам FP (их нет в некоторых других местах). - person Stefan Pavikevik; 26.11.2020
comment
Проект, над которым я работаю, ни в коем случае не является чистым FP, и во многих местах он создает исключения (иногда мне хочется просто изменить все это :D), поэтому возникла дискуссия о том, какой из них мы должны предпочесть ( бросание против Future.exception), поэтому я разместил этот вопрос. - person Stefan Pavikevik; 26.11.2020

Насколько я понимаю FP, исключения не выбрасываются. Это, как вы сказали, побочный эффект. Вместо этого исключения представляют собой значения, которые обрабатываются в какой-то момент выполнения программы.

Cats (и я уверен, что другие библиотеки тоже) используют эту технику (https://github.com/typelevel/cats/blob/master/core/src/main/scala/cats/ApplicativeError.scala).

Таким образом, вызов flatMap позволяет содержать исключение в удовлетворенном Future здесь и обрабатывать его на более позднем этапе выполнения программы, где также может происходить другая обработка значений исключения.

person randbw    schedule 25.11.2020