...

суббота, 16 августа 2014 г.

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 и в чем я допустил упущения.

Спасибо за внимание.

This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий