С теоретической точки зрения, если мы уберем часть исключений (они все равно не могут быть аргументированы использованием теории категорий), то эти две операции полностью идентичны, пока выбранная вами конструкция (в вашем случае Твиттер 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))
Так что да, как вы уже намекнули, эти два подхода идентичны.
Тем не менее, у меня есть три комментария по этому поводу, учитывая, что мы включаем в список исключения:
- Будьте осторожны — когда дело доходит до генерации исключений, 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!
- Как упоминал @randbw, создание исключений не является идиоматичным для FP и нарушает такие принципы, как чистота функций и ссылочная прозрачность значений.
Scala и Twitter Future
упрощают создание исключения — пока это происходит в контексте Future
, исключение не всплывет, а вместо этого приведет к сбою Future
. Однако это не означает, что буквальное использование их в вашем коде должно быть разрешено, потому что это разрушает структуру ваших программ (аналогично тому, как это делают операторы GOTO или операторы break в циклах и т. д.).
Предпочтительная практика состоит в том, чтобы всегда оценивать каждый путь кода как значение, а не разбрасывать бомбы, поэтому лучше использовать flatMap в (неудачный) Future
, чем сопоставлять какой-либо код, который бросает бомбу.
- Помните о ссылочной прозрачности.
Если вы используете 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
Future.exception
, вероятно, быстрее, чем создание и перехват исключения. - В идеале вы никогда не должны генерировать исключения самостоятельно, а лучше поднимать ошибочные значения внутри Future с помощью комбинаторов, таких какexception
. - person Luis Miguel Mejía Suárez   schedule 25.11.2020