Добрый вечер господа читатели. Сегодня мне хотелось бы пролить немного света на такую замечательную часть scala core под названием Future. Собственно существует документация на официальном сайте, но там идет объяснение как работать с ним при помощи event driven подхода. Но при это Future является также и монадой. И в данной статье я хотел привести примеры и немого растолковать как их надо использовать в этом ключе (а точнее свое видение этого вопроса). Всех желающим ознакомится с вопросом прошу под кат.
Чего здесь не будет
В этой статье я не собираюсь рассказывать про callback'и, promise'ы, про то как достигается эта отличная модель конкуренции или еще что то в этом духе.
Что будет
А писать я буду как раз про функции которые дают Future поведение монады.
map
Итак обратимся к api:
def map[S](f: (T) ⇒ S)(implicit executor: ExecutionContext): Future[S]
Что же делает map? При удачном исполнении Future, будет выполнена функция f с переданным в нее результатом. То есть:
Future(5) map (2*) map println
В конечно итоге этот код выведет 10 на экран как и ожидалось. Если же результат будет Failure, то функция не будет исполнена:
Future(5) map (_ / 0) map println
Вторая функция выбросит исключение и ничего не будет выведено на экран.
Итак для чего же стоит использовать map? — для последовательных операций, которые требуют результат предыдущей.
flatMap
Искушенный читатель понимает что монада может быть внутри монады и надо это дело как то исправлять:
def flatMap[S](f: (T) ⇒ Future[S])(implicit executor: ExecutionContext): Future[S]
flatMap берет функцию, которая возвращает другой Future, и возвращает его.
def f(a: Int): Future[Int] = Future(a * 5)
Future(2) flatMap (f) map println
На экран будет выведено число 10. В случае с неудачным исполнением, поведение такое же как и в map.
flatMap следует использовать в случае цепочных операций, которые возвращают Future.
for
Не надо лететь и гневно писать, что for это не функция, а синтаксическая конструкция. Да, я знаю, но не рассказать про нее было бы глупо
Коль уж мы рассмотрели map и flatMap, нужно рассмотреть использование с for-comprehensions.
Давайте рассмотрим некоторые плохой и хороший примеры использования for с Future:
//Плохо
for {
a <- longComputations1()
b <- longComputations2()
c <- longComputations3()
} yield a*b*c
//Лучше
val f1 <- longComputations1()
val f2 <- longComputations2()
val f3 <- longComputations3()
for {
a <- f1
b <- f2
c <- f3
} yield a*b*c
Результат обоих операций будет одинаков, но…
for {
a <- longComputations1()
b <- longComputations2()
c <- longComputations3()
} yield a*b*c
развернется в:
longComputations1().flatMap { a =>
longComputations2().flatMap { b =>
longComputations3().flatMap { c =>
a*b*c
}
}
}
То есть все это дело будет вызвано последовательно а не параллельно. Второй вариант решает эту проблему но не самым лучшим способом, поэтому только лишь лучше.
zip
Ранее была рассмотрена проблема использования нескольких Future одновременно. Что же, zip решает эту проблему. Он берет два Future и упаковывает их результаты в Tuple2. И вот наш пример сверху как запишется теперь:
longComputations1() zip longComputations2() zip longComputations3() map {
case ((a, b), c) => a * b * c
}
Лично по моему, все куда чище и проще.
filter и withFilter
def filter(p: (T) ⇒ Boolean)(implicit executor: ExecutionContext): Future[T]
final def withFilter(p: (T) ⇒ Boolean)(implicit executor: ExecutionContext): Future[T]
Тут все логично, берем результат, тестируем его, и если оно не подходит то дальше будет Future с Failed, в котором будет запаковано NoSuchElementException.
recover и recoverWith
Коль уж у нас код исполняется асинхронно, то нам нужен асинхронный контроль за исключениями. И вот оно:
def recover[U >: T](pf: PartialFunction[Throwable, U])(implicit executor: ExecutionContext): Future[U]
def recoverWith[U >: T](pf: PartialFunction[Throwable, Future[U]])(implicit executor: ExecutionContext): Future[U]
В случае исключительной ситуации будет вызвана частичная функция, которая в одном случае должна возвращать значение, в другом Future.
Future(5) map (_ / 0) recover { case _ => 0 } map println
Тут у нас будет обработано исключение и выведется 0.
forEach
По сути представляет из себя map, только результат не пакуется в новый Future, а просто возвращается Unit:
def foreach[U](f: (T) ⇒ U)(implicit executor: ExecutionContext): Unit
По сути предыдущие примеры не совсем корректны и лучше было бы написать:
Future(5) map (2*) foreach println
Тут мы избежали одного лишнего создания Future.
Более общий пример
Итак мы имеем:
def f1: Future[Double]
def f2: Future[Double]
def f3(a: Double): Future[Double]
def f4: Future[Double]
def f5(a: Double, b: Double, c: Double): Future[Double]
Мы знаем, что в f3 должен быть передан результат выполнения f2, а в f5 должны быть переданы результаты выполнения f1, f3 и f4. И в конце результат должен быть выведен в стандартный поток вывода. Также известно, что f3 может выбросить исключение, и в этом случае должен быть возвращен 0.
Поехали:
val r1 = f1()
val r2 = f2() flatMap (f3) recover { case _: Exception => 0 }
var r3 = f4()
for {
a <- r1
b <- r2
c <- r3
} yield f5(a, b, c)
Я же предпочитаю:
(f1 zip f4)
.zip(f2() flatMap (f3) recover { case _: Exception => 0 }) flatMap {
case ((a, b), c) => f5(a, b, c)
}
А как бы записали вы?
Послесловие
Итак я постарался описать монадные функции Future, и лично я считаю, что мне удалось. Конечно же еще, что описать, например класс-helper, в котором есть очень интересные и важные функции, но это уже материал другой статьи.
Прошу писать в комментариях то, как вы используете Future и в чем я допустил упущения.
Спасибо за внимание.
Автор: eld0727