Future
Scala oferuje łatwy sposób zarządzania równoległym wykonywaniem kodu w postaciFuture
i callbacków. to
specjalny obiekt, który służy do przechowywania wartości (zwykle wyniku pewnych obliczeń), która może być dostępna w przyszłości.
w przyszłości. Callback jeśli metoda, która zostanie uruchomiona, gdy Future
zostanie zakończona. Aby użyć Futures w naszej
musimy najpierw zaimportować pakiet scala.concurrent
oraz dostarczyć ExecutionContext
. Jeśli pracujesz z aplikacją Play-framework, najłatwiejszym sposobem uzyskania kontekstu wykonania jest użycie domyślnej puli wątków Play. Możesz uzyskać do niej dostęp poprzez @Inject()(implicit ec:ExecutionContext)
.
Aby wykonać blok kodu współbieżnie musimy wywołać metodę Future.apply
jak pokazano w przykładzie poniżej.
val futureValue: Future[TypeA] = Future {
someCode() // Some method that returns value of type TypeA
}
Callbacki
Metoda Future.apply
zwraca obiekt Future
zawierający wartość zwrotną funkcji functionsomeCode()
. Aby uzyskać do niego dostęp musimy dodać jakiś callback. Możliwości jest tu kilka:
onComplete
foreach
map
Pierwszy z nich – onComplete
– daje nam dostęp do dwóch przypadków:
Success[T]
– który przechowuje wartość zwrotną pomyślnie zakończonegoFuture
Failed[T]
– które przechowują wartość rzuconego wyjątku w przypadku niepowodzeniaFuture
futureValue.onComplete {
case Success(value) => someCode(value) // Some computation performed on value with type TypeA
case Failure(exception) => println("Error: " + exception.getMessage)
}
Druga i trzecia opcja pozwala nam na obsługę tylko udanego wyniku. Zarówno onComplete
iforeach
mają typ wyniku Unit
co uniemożliwia nam łączenie ich z innymi callbackami.
Są one głównie używane do efektów ubocznych.
Ostatnia opcja – map
– daje nam dostęp do mapowania wyników do dowolnego typu i zwraca je w nowym Future po zakończeniu oryginalnej przyszłości.
val futureValue: Future[TypeA] = Future {
someCode() // Some method that return value of type TypeA
}
val futureValue2: Future[TypeB] = futureValue.map{ value =>
someOtherMethod(value) // Some other method of type TypeA => TypeB
}
Wykonanie równoległe
Czasami potrzebujemy dostępu do wartości dwóch lub więcej różnych Future
. Jednym ze sposobów radzenia sobie z tym problemem jest zagnieżdżanie callbacka jednego future w innym callbacku – jak pokazano na poniższym przykładzie.
val future1: Future[TypeA] = Future{
someCode()
}
val future2: Future[TypeB] = Future{
someOtherCode()
}
val future3: Future[Future[TypeC]] = future1.map{ value1 =>
future2.map{ value2 =>
someMethodHere(value1, value2) // method of type (TypeA, TypeB) => TypeC
} // returns Future[TypeC]
}
Jak widzimy, nie jest to najczystsze podejście. Co ważniejsze, typem zwrotnym naszych zagnieżdżonych map będzie zagnieżdżona Future
w co znacznie utrudni dostęp do jej wyniku. Z tego powodu Future
mają kombinatory flatMap
i withFilter
, które pozwalają na użycie ich w for-comprehensions. Ten sam kod co powyżej, ale z for-comprehension wyglądałby tak:
val future1: Future[TypeA] = Future{
someCode()
}
val future2: Future[TypeB] = Future{
someOtherCode()
}
val future: Future[TypeC] = for {
value1 <- future1
value2 <- future2
} yield {
someMethodHere(value1, value2) // method of type (TypeA, TypeB) => TypeC
}
Kiedy wykonuje się Future?
Używając Future
zarówno z callbackami, for-comprehension czy innymi kombinatorami, bardzo ważne jest, aby upewnić się, że używamy go poprawnie. Jeśli porównamy dwa bloki kodu:
// Block 1
val future1: Future[TypeA] = Future{
someCode()
}
val future2: Future[TypeB] = Future{
someOtherCode()
}
val future = for {
value1 <- future1
value2 <- future2
} yield {
...
}
// Block 2
val future = for {
value1 <- Future{someCode()}
value2 <- Future{someOtherCode()}
} yield {
...
}
Używając Future zarówno z callbackami, for-comprehension czy innymi kombinatorami, bardzo ważne jest, aby upewnić się, że używamy go poprawnie. Jeśli porównamy dwa bloki kodu, na pierwszy rzut oka może się wydawać, że oba robią to samo: uruchamiają dwie metody równolegle do siebie. W rzeczywistości tylko pierwsza z nich będzie działać współbieżnie. Ponieważ for comprehensions są rozszerzone na (czasami) zagnieżdżoneforeach
, map
, flatMap
and withFilter
drugi blok spowodowałby uruchomienie kodu podobnego do tego.
Future{someCode()}.flatMap{value1 => Future{someOtherCode()}.map{value2 => ... }}
Co oznacza, że najpierw uruchomiona zostanie funkcja someCode()
po jej zakończeniu someOtherCode()
Scala rozpoczyna wykonywanie kodu w Future.apply
, gdy instancja jest tworzona, a nie gdy jest oceniana. Przypisując future do wartości poza for-comprehensions, uruchamiamy oba, a następnie przypisujemy im metody callback w for-comprehensions.