Back to the Scala Future

в 16:01, , рубрики: functional programming, scala, scala future, функциональное программирование

Back to the Scala Future
Добрый вечер господа читатели. Сегодня мне хотелось бы пролить немного света на такую замечательную часть 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

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js