Future adalah wadah untuk hasil komputasi yang dikerjakan pada thread lain, sehingga thread utama tidak perlu diam menunggu pekerjaan yang lambat selesai. Topik ini membahas cara membuat Future pada ExecutionContext.global, mengubah isinya dengan map dan flatMap alih-alih membaca nilainya secara langsung, menjembatani alur asinkron ke program demo dengan Await, serta menangani kegagalan dengan recover dan onComplete. Setelah membacanya, komputasi yang berat dapat dipindahkan dari thread utama tanpa menambah satu pustaka pun, karena Future sudah tersedia di pustaka standar Scala.
Membuat future pada ExecutionContext#
Future menerima blok kode dan menjadwalkannya pada ExecutionContext, yaitu penyedia thread yang mengeksekusi tugas-tugas asinkron. Bentuk yang paling umum adalah ExecutionContext.global, sebuah kumpulan thread bawaan JVM yang ukurannya menyesuaikan jumlah inti prosesor. Ekspresi Future { ... } mengembalikan objek Future[A] seketika, sedangkan isi bloknya baru selesai beberapa saat kemudian. Tipe A adalah tipe hasil blok tersebut, misalnya Future[Long] untuk blok yang menghitung angka bertipe Long. Program berikut memindahkan satu perhitungan ke ExecutionContext.global, lalu mencetak hasilnya dari thread utama.
// Komputasi dipindahkan ke ExecutionContext.global, thread utama menunggu hasilnya
import scala.concurrent.{Await, Future}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.*
@main def jumlahKuadrat(): Unit =
val kerja: Future[Long] = Future {
(1L to 100L).map(n => n * n).sum
}
val hasil: Long = Await.result(kerja, 5.seconds)
println(s"Jumlah kuadrat 1 sampai 100: $hasil")# menjalankan program kuadrat.scala
scala-cli run kuadrat.scalaJumlah kuadrat 1 sampai 100: 338350Blok yang diberikan kepada Future dieksekusi pada thread milik executor, bukan pada thread utama, tetapi keluarannya tetap deterministik karena pencetakan dilakukan setelah Await.result kembali. Tanpa baris Await.result itu, program @main dapat berakhir sebelum bloknya selesai, sehingga hasilnya tidak pernah dicetak. Angka 338350 adalah jumlah kuadrat satu sampai seratus, yang dapat dihitung ulang dengan rumus . Tipe Future[Long] pada val kerja dinyatakan secara eksplisit agar hasil komputasinya jelas dari definisinya, tanpa perlu menyelusuri isi bloknya.
Transformasi dengan map dan flatMap#
Isi sebuah future tidak dibaca secara langsung, melainkan diubah melalui metode pada objek Future itu sendiri. Metode map menerima fungsi biasa dan menghasilkan future baru yang isinya sudah berubah, sedangkan flatMap menerima fungsi yang menghasilkan future, sehingga rantai tahapan tidak menumpuk menjadi Future[Future[Int]]. Kedua transformasi berjalan tanpa memblokir thread pemanggilnya, dan hasil akhirnya adalah future baru yang siap dirantai ke tahap berikutnya. Program berikut menunjukkan keduanya pada satu nilai awal yang sama.
// map dan flatMap mengubah isi future tanpa memblokir thread utama
import scala.concurrent.{Await, Future}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.*
@main def transformasi(): Unit =
def duaKali(n: Int): Future[Int] = Future(n * 2)
val awal: Future[Int] = Future(24)
val digandakan: Future[Int] = awal.map(n => n * 2)
val dirantai: Future[Int] = awal.flatMap(duaKali).flatMap(duaKali)
println(s"map: 24 dikali 2 menjadi ${Await.result(digandakan, 5.seconds)}")
println(s"flatMap: 24 lewat duaKali dua kali menjadi ${Await.result(dirantai, 5.seconds)}")# menjalankan program transformasi.scala
scala-cli run transformasi.scalamap: 24 dikali 2 menjadi 48
flatMap: 24 lewat duaKali dua kali menjadi 96Pada baris pertama, map(n => n * 2) mengubah Future(24) menjadi future yang isinya 48, dan thread utama tidak pernah memegang angka 48 secara langsung. Pada baris kedua, flatMap(duaKali) baru menjalankan duaKali setelah awal selesai, karena future yang harus dikembalikannya membutuhkan nilai tersebut. Apabila map yang digunakan untuk fungsi duaKali, hasilnya bertipe Future[Future[Int]] dan rantai tahapannya menjadi semakin bertingkat. Interpolasi string pada println baru dievaluasi setelah Await.result menghasilkan angkanya, sehingga urutan pencetakan terjamin.
Pola yang sama berlaku untuk tipe apa pun di dalam future. Konversi satuan berikut menghasilkan Future[Double] tanpa membuat program menggantung di tengah perhitungannya.
val suhu: Future[Int] = Future(24)
val fahrenheit: Future[Double] = suhu.map(c => c * 9.0 / 5 + 32)Await sebagai jembatan#
Await.result(future, durasi) memblokir thread pemanggil sampai future selesai, lalu mengembalikan isinya. Apabila batas waktu terlampaui sebelum future selesai, eksepsi TimeoutException dilempar, sehingga program demo tidak pernah menggantung tanpa batas. Bentuk yang kedua, Await.ready, mengembalikan future itu sendiri setelah selesai, dan hasilnya diperiksa melalui .value yang bertipe Option[Try[A]]. Try, yaitu wadah yang memuat hasil atau eksepsi, muncul di sini sebagai Success dan Failure. Setelah Await.ready kembali, .value pasti berisi Some, karena future-nya sudah selesai, sehingga pencocokan pola berikut selalu berhasil.
Await.ready(kerja, 5.seconds)
kerja.value match
case Some(Success(nilai)) => println(s"Selesai: $nilai")
case Some(Failure(e)) => println(s"Gagal: ${e.getClass.getSimpleName}")Kedua fungsi Await adalah jembatan untuk program demo kecil dan pengujian, bukan gaya penulisan aplikasi. Thread yang diblokir oleh Await tidak dapat mengerjakan tugas lain selama menunggu, sehingga aplikasi server yang memblokir satu thread untuk setiap panggilan asinkron cepat kehabisan thread. Gaya penulisan aplikasi memindahkan pemrosesan hasil ke dalam transformasi dan callback, lalu membiarkan kerangka kerjanya menunggu di ujung alur.
Kegagalan dan pemulihan#
Future yang bloknya melempar eksepsi berubah menjadi future gagal yang menyimpan Throwable tersebut. Membaca hasilnya dengan Await.result melempar kembali eksepsi itu, sehingga penanganannya perlu dilakukan pada level future, bukan dengan try di sekitar pembacaannya. Metode recover menerima fungsi sebagian yang mencocokkan jenis kegagalan tertentu dan menghasilkan nilai pengganti, sedangkan onComplete menerima callback yang mendapat laporan akhir berupa Success atau Failure. Program berikut membuat future yang gagal di tengah perhitungan, memeriksa jenis kegagalannya, lalu memulihkannya menjadi nilai pengganti.
// Future yang gagal dipulihkan dengan recover, jenis errornya diperiksa lewat failed
import scala.concurrent.{Await, Future}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.*
@main def pemulihan(): Unit =
val kerja: Future[Int] = Future {
val teks: List[String] = List("12", "30", "tujuh", "5")
teks.map(a => a.toInt).sum
}
val aman: Future[Int] = kerja.recover { case _: NumberFormatException => -1 }
println(s"Jenis error: ${Await.result(kerja.failed, 5.seconds).getClass.getSimpleName}")
println(s"Hasil pemulihan: ${Await.result(aman, 5.seconds)}")# menjalankan program pemulihan.scala
scala-cli run pemulihan.scalaJenis error: NumberFormatException
Hasil pemulihan: -1Teks “tujuh” pada daftar input membuat toInt melempar NumberFormatException, dan future kerja selesai dengan kegagalan karena itu. Metode failed mengembalikan Future[Throwable], yaitu future yang isinya justru kegagalannya, sehingga jenis error dapat dibaca dengan Await.result dan pencocokan pola. Pola case _: NumberFormatException pada recover hanya menangkap kegagalan konversi angka, sedangkan jenis kegagalan lain tetap diteruskan ke future hasilnya. Keluaran tetap deterministik meskipun komputasinya berjalan pada thread lain, karena kedua pencetakan dilakukan dari thread utama setelah masing-masing future selesai.
Bentuk onComplete menempatkan seluruh penanganan pada satu callback, dengan cabang Success untuk hasil dan Failure untuk kegagalan.
kerja.onComplete {
case Success(nilai) => println(s"Selesai dengan $nilai")
case Failure(e) => println(s"Gagal: ${e.getClass.getSimpleName}")
}Callback pada onComplete dieksekusi pada thread milik ExecutionContext, sehingga urutannya terhadap thread utama tidak dijamin. Karena itulah program demo pada topik ini menunggu future selesai terlebih dahulu dengan Await, lalu mencetak dari thread utama. Aturan yang sama menjaga semua keluaran contoh konkurensi tetap dapat direproduksi secara persis.
Ringkasan#
Future { ... }menjadwalkan blok kode padaExecutionContext.globaldan mengembalikan objeknya seketika, sedangkan hasilnya baru tersedia kemudian.mapdanflatMapmengubah isi future tanpa memblokir thread, dan tidak ada cara membaca isinya secara langsung pada API-nya.Await.resultdanAwait.readyadalah jembatan pemblokir untuk program demo kecil dan pengujian, denganTimeoutExceptionsebagai batas waktunya.- Future yang gagal menyimpan
Throwable, yang dapat dibaca melaluifailed, dipulihkan denganrecover, atau dilaporkan melaluionComplete.
Referensi#
- Futures and Promises — Scala Documentation
— Penjelasan resmi model
Future,ExecutionContext, transformasi, dan kombinatornya. - Functional Concurrency — Scala 3 Book
— Bab konkurensi buku Scala 3 yang memperkenalkan
FuturedanAwait. - Future — Scala Standard Library
— Dokumentasi API
map,flatMap,recover,failed, danonComplete.