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ńczonego Future
  • Failed[T] – które przechowują wartość rzuconego wyjątku w przypadku niepowodzenia Future
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 Futuremają 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.