Menyusun beberapa tahap asinkron berarti menyusun flatMap di dalam flatMap, dan for-comprehension menjaga susunan itu tetap terbaca sebagai blok vertikal. Topik ini membahas for-comprehension atas future yang tahapannya saling bergantung, perbedaannya dengan future yang sudah dibuat sebelumnya, penggabungan future yang independen dengan zip dan Future.sequence, serta penanganan kegagalan di dalam alur. Setelah membacanya, alur asinkron bertahap dapat ditulis tanpa piramida callback, dan kondisi yang menuntut eksekusi berurutan atau bersamaan dapat dinyatakan dengan sengaja.
Generator bergantung menjadi flatMap berurutan#
For-comprehension atas future mengikuti desugaring yang sama dengan for-comprehension atas koleksi, yaitu satu generator menjadi map dan generator berganda menjadi flatMap bersarang. Perbedaannya terletak pada waktu: sisi kanan generator berikutnya baru dievaluasi setelah generator sebelumnya selesai, karena sisi kanannya boleh menggunakan ikatan yang sudah tersedia. Apabila sisi kanan itu berupa pemanggilan fungsi yang membangun future dari hasil generator sebelumnya, eksekusinya benar-benar berurutan. Program berikut menyusun dua tahap yang keduanya bergantung, yaitu membaca data lalu menghitung rata-ratanya.
// Tahap kedua baru dibuat setelah tahap pertama selesai, karena datanya bergantung
import scala.concurrent.{Await, Future}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.*
@main def alur(): Unit =
val bacaData: Future[List[Int]] = Future(List(80, 65, 90, 55, 70))
def rataRata(nilai: List[Int]): Future[Int] =
Future(nilai.sum / nilai.size)
val laporan: Future[String] =
for
data <- bacaData
rata <- rataRata(data)
yield s"Rata-rata nilai ujian: $rata"
println(Await.result(laporan, 5.seconds))# menjalankan program alur.scala
scala-cli run alur.scalaRata-rata nilai ujian: 72Baris rata <- rataRata(data) adalah inti contoh ini, karena rataRata tidak dapat dipanggil sebelum data tersedia. Desugaring dari for di atas adalah pemanggilan berikut. Dalam bentuk bersarang inilah piramida callback yang dikenal sulit dibaca.
bacaData.flatMap(data =>
rataRata(data).map(rata =>
s"Rata-rata nilai ujian: $rata"
)
)Setiap tahap tambahan menambah satu lapis bersarang pada bentuk flatMap, sedangkan bentuk for menambah satu baris generator. Angka 72 pada keluaran berasal dari pembagian bulat 360 dengan 5, yaitu jumlah kelima nilai ujian dibagi dengan banyaknya peserta. Pencetakan dilakukan dari thread utama setelah Await.result kembali, sehingga keluaran tetap sama persis meskipun kedua tahap berjalan pada thread yang berbeda-beda.
Future yang sudah dibuat tidak menunggu for#
Generator berganda tidak selalu berarti eksekusi berurutan. Apabila kedua sisi kanan generator adalah future yang sudah dibuat sebelumnya, keduanya sudah berjalan bersamaan sejak pembuatannya, dan for hanya mengurutkan pengambilan hasilnya menjadi satu nilai akhir. Kode berikut menggambarkan kondisi itu.
val jumlahA: Future[Int] = Future(hitungA()) // langsung dijadwalkan
val jumlahB: Future[Int] = Future(hitungB()) // langsung dijadwalkan
for
a <- jumlahA
b <- jumlahB
yield a + bKedua future pada contoh itu mulai dijadwalkan pada baris val-nya, bukan pada baris generatornya, sehingga tidak ada pekerjaan yang ditunda oleh for. Aturan yang membedakannya sederhana: generator yang kanannya merujuk val yang sudah ada berjalan bersamaan, sedangkan generator yang kanannya membangun future baru dari ikatan sebelumnya berjalan berurutan. Keduanya sah, tetapi keputusannya perlu disengaja, karena salah membacanya membuat alur yang seharusnya berurutan berjalan tidak berurutan, atau sebaliknya membuang kesempatan menjalankan pekerjaan secara bersamaan.
Menggabungkan future yang independen#
Apabila hasil-hasil yang dibutuhkan tidak saling bergantung, zip dan Future.sequence adalah penyusunnya. Metode zip menggabungkan dua future menjadi Future[(A, B)], sedangkan Future.sequence mengumpulkan List[Future[A]] menjadi Future[List[A]], dan keduanya menghasilkan future gagal apabila salah satu elemennya gagal. Bentuk for juga sah untuk dua future yang sudah dibuat, tetapi zip menyatakan niatnya secara eksplisit, sedangkan Future.sequence tetap ringkas untuk jumlah future yang berubah-ubah. Program berikut menjalankan dua penghitungan independen sekaligus, lalu mengumpulkan tiga future bernilai tetap.
// Future yang independen digabung dengan zip, daftar future dikumpulkan sequence
import scala.concurrent.{Await, Future}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.*
@main def gabungan(): Unit =
val jumlahGenap: Future[Int] = Future((2 to 100 by 2).sum)
val jumlahGanjil: Future[Int] = Future((1 to 99 by 2).sum)
val berpasangan: Future[(Int, Int)] = jumlahGenap.zip(jumlahGanjil)
val satuan: List[Future[Int]] = List(Future(2), Future(4), Future(6))
val terkumpul: Future[List[Int]] = Future.sequence(satuan)
val (genap, ganjil) = Await.result(berpasangan, 5.seconds)
println(s"Genap $genap, ganjil $ganjil, total ${genap + ganjil}")
println(Await.result(terkumpul, 5.seconds))# menjalankan program gabungan.scala
scala-cli run gabungan.scalaGenap 2550, ganjil 2500, total 5050
List(2, 4, 6)Angka pada keluaran dapat dihitung ulang, yaitu 2550 untuk jumlah bilangan genap 2 sampai 100, 2500 untuk jumlah bilangan ganjil 1 sampai 99, dan 5050 untuk totalnya. Baris kedua adalah hasil Future.sequence yang mengembalikan bentuk Future[List[Int]]. Pasangan hasil zip diurai kembali melalui pola val (genap, ganjil), sehingga kedua penghitungan yang berjalan bersamaan tetap diperiksa satu per satu dari thread utama. Untuk perbandingan, bentuk for di bawah ini menghasilkan nilai akhir yang sama, karena kedua future-nya memang sudah dibuat sebelumnya.
for
genap <- jumlahGenap
ganjil <- jumlahGanjil
yield genap + ganjilPilihan di antara keduanya hanya masalah keterbacaan pada jumlah kecil, tetapi begitu future yang harus dikumpulkan bertambah banyak atau berasal dari daftar, Future.sequence menjadi satu-satunya bentuk yang ringkas.
Penanganan kegagalan dalam for-comprehension#
Kegagalan pada satu tahap diteruskan ke hasil akhir for-comprehension, sehingga generator setelahnya tidak dijalankan dan alur selesai lebih awal dengan kegagalan itu. Perilakunya sama dengan flatMap pada Try, yaitu sisa rantai dilewati begitu ada kegagalan. Karena hasil akhirnya adalah future biasa, recover ditempelkan di ujung alur untuk memulihkan kegagalan apa pun yang berasal dari tahap mana pun. Program berikut membuat alur dua tahap dengan transformasi yang gagal, lalu memulihkannya.
// Kegagalan pada satu tahap membuat seluruh alur for gagal, lalu dipulihkan di ujungnya
import scala.concurrent.{Await, Future}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.*
@main def kegagalan(): Unit =
val bacaData: Future[List[String]] = Future(List("12", "30", "tujuh", "5"))
val alur: Future[Int] =
for
teks <- bacaData
angka = teks.map(a => a.toInt).sum
yield angka
val aman: Future[Int] = alur.recover { case _: NumberFormatException => -1 }
println(s"Jenis error: ${Await.result(alur.failed, 5.seconds).getClass.getSimpleName}")
println(s"Hasil pemulihan: ${Await.result(aman, 5.seconds)}")# menjalankan program kegagalan.scala
scala-cli run kegagalan.scalaJenis error: NumberFormatException
Hasil pemulihan: -1Baris angka = teks.map(a => a.toInt).sum menggunakan tanda =, bukan <-, sehingga baris itu adalah ikatan biasa yang disisipkan ke dalam desugaring tanpa menambah generator. Konversi “tujuh” dengan toInt melempar NumberFormatException di dalam map, sehingga future alur selesai dengan kegagalan, dan recover di ujungnya mengubah kegagalan tersebut menjadi nilai -1. Letak recover menentukan cakupannya, karena recover yang ditempel pada bacaData hanya menangkap kegagalan tahap pertama, sedangkan kegagalan pada transformasi berikutnya tetap lolos. Dengan menempelkan recover pada hasil akhir, satu penanganan menutup seluruh alur, dan pencetakan tetap dilakukan dari thread utama setelah Await.result kembali.
Ringkasan#
- For-comprehension atas future diterjemahkan menurut aturan desugaring menjadi
flatMapdanmapbersarang, sehingga bentuknya setara dengan piramida callback tetapi terbaca vertikal. - Generator yang kanannya membangun future dari hasil generator sebelumnya berjalan berurutan, sedangkan future yang sudah dibuat sebelumnya berjalan bersamaan sejak pembuatannya.
zipmenggabungkan dua future independen menjadi pasangan, danFuture.sequencemengumpulkan daftar future menjadi satu future berisi daftar.- Kegagalan pada satu tahap diteruskan ke hasil akhir for, dan
recoveryang ditempel di ujung alur memulihkannya menjadi nilai.
Referensi#
- Futures and Promises — Scala Documentation
— Bagian kombinasi future yang memuat for-comprehension,
zip, danFuture.sequence. - For Comprehensions — Scala Tour
— Aturan desugaring for menjadi
flatMapdanmappada dokumentasi resmi. - Future — Scala Standard Library
— Dokumentasi API
zip,sequence, danfailed.