Try dan Either

Kegagalan sebagai data dan eksepsi yang tetap di batas sistem saja.

Daftar isi

Operasi yang dapat gagal di Scala dinyatakan sebagai nilai, bukan sebagai lompatan kontrol yang tersembunyi di tengah program. Tipe Try menangkap eksepsi (exception) dan menjadikannya nilai Success atau Failure, sedangkan tipe Either menyatakan kegagalan yang bermakna melalui Left yang membawa data error. Topik ini membahas keduanya beserta recover, left.map, dan fold, lalu menutup dengan penempatan eksepsi pada batas sistem saja, yaitu satu-satunya tempat eksepsi muncul pada program yang rapi.

Try: eksepsi menjadi nilai#

Try[A] mengevaluasi sebuah ekspresi dan menghasilkan Success[A] apabila ekspresi itu selesai tanpa eksepsi, atau Failure yang membungkus eksepsinya apabila eksepsi terlempar. Eksepsi itu sendiri menjadi data yang tersimpan pada nilai, sehingga penerima hasil dapat memeriksanya seperti nilai lain tanpa blok try dan catch di setiap lapisan. Bentuk ini cocok untuk operasi yang kegagalannya bersumber dari luar kendali program, misalnya pembacaan berkas atau penguraian teks masukan pengguna.

Metode toInt pada String adalah contoh operasi semacam itu, karena teks yang bukan bilangan membuat metode itu melempar NumberFormatException. Program berikut menguraikan daftar masukan pengguna dengan Try.

scala parsing-try.scala
// Try: operasi yang melempar eksepsi menjadi nilai Success atau Failure
import scala.util.Try

@main def parsingTry(): Unit =
  val masukan: List[String] = List("42", "tiga", "100")

  for teks <- masukan do
    println(Try(teks.toInt))

  // recover mengganti Failure dengan nilai cadangan
  val cadangan = Try("tiga".toInt).recover { case _: NumberFormatException => 0 }
  println(cadangan)
bash
# menjalankan program parsing-try.scala
scala-cli run parsing-try.scala
text Keluaran
Success(42)
Failure(java.lang.NumberFormatException: For input string: "tiga")
Success(100)
Success(0)

Tiga baris pertama keluaran memperlihatkan bahwa Try mengubah perilaku toInt yang melempar eksepsi menjadi nilai biasa, yaitu Success(42) dan Success(100) untuk masukan yang sah serta Failure yang membawa NumberFormatException untuk “tiga”. Baris keempat menunjukkan recover yang mengganti Failure dengan nilai cadangan, dan hasilnya menjadi Success(0) karena kegagalan itu sudah ditangani. Program tetap berjalan sampai selesai meskipun ada masukan yang tidak sah, karena tidak ada eksepsi yang merambat keluar dari nilai Try.

Either: kegagalan yang bermakna#

Either[E, A] menyatakan hasil yang berada pada salah satu dari dua cabang, yaitu Left[E] untuk kegagalan dan Right[A] untuk keberhasilan. Perbedaannya dengan Try terletak pada isi cabang kegagalan, karena Left tidak harus berupa eksepsi dan dapat bertipe khusus yang dirancang sendiri, misalnya enum berisi kasus-kasus kegagalan. Tipe seperti ini membuat semua kemungkinan kegagalan terdaftar pada definisi tipenya, dan setiap pemroses wajib menanganinya satu per satu.

Metode map mengubah isi Right tanpa menyentuh Left, sedangkan left.map menerjemahkan Left tanpa menyentuh Right. Metode fold menyatukan kedua cabang menjadi satu tipe keluaran dengan menerima dua fungsi, satu untuk Left dan satu untuk Right. Program berikut menggabungkan Try dan Either pada penguraian nilai persen.

scala either-kegagalan.scala
// Either: kegagalan sebagai data yang bermakna, bukan eksepsi
import scala.util.{Failure, Success, Try}

enum Kesalahan:
  case FormatTidakValid(teks: String)
  case DiLuarJangkauan(nilai: Int)

def parsePersen(teks: String): Either[Kesalahan, Int] =
  Try(teks.toInt) match
    case Success(n) =>
      if n >= 0 && n <= 100 then Right(n) else Left(Kesalahan.DiLuarJangkauan(n))
    case Failure(_) =>
      Left(Kesalahan.FormatTidakValid(teks))

@main def eitherKegagalan(): Unit =
  println(parsePersen("75"))
  println(parsePersen("tiga"))
  println(parsePersen("250"))

  // left.map menerjemahkan Left tanpa menyentuh Right
  val berpesan = parsePersen("250").left.map:
    case Kesalahan.FormatTidakValid(teks) => s"format tidak valid: $teks"
    case Kesalahan.DiLuarJangkauan(n)     => s"nilai $n di luar jangkauan 0 sampai 100"

  println(berpesan)

  // fold menyatukan kedua cabang menjadi satu tipe keluaran
  val pesan: String = parsePersen("75").fold(
    k => k.toString,
    n => s"nilai $n diterima"
  )
  println(pesan)
bash
# menjalankan program either-kegagalan.scala
scala-cli run either-kegagalan.scala
text Keluaran
Right(75)
Left(FormatTidakValid(tiga))
Left(DiLuarJangkauan(250))
Left(nilai 250 di luar jangkauan 0 sampai 100)
nilai 75 diterima

Fungsi parsePersen menghasilkan tiga bentuk keluaran yang berbeda dari tanda tangan yang sama. Masukan “75” menghasilkan Right(75), masukan “tiga” menghasilkan Left(FormatTidakValid(tiga)), dan masukan “250” menghasilkan Left(DiLuarJangkauan(250)). Dua kegagalan terakhir tidak lagi berupa teks bebas, melainkan data bertipe yang membawa penyebabnya, sehingga left.map dapat menerjemahkannya menjadi pesan yang konsisten. Metode fold pada baris terakhir menyatukan kedua cabang menjadi satu String, yaitu bentuk yang biasanya dibutuhkan oleh lapisan penampilan.

Eksepsi tetap di batas sistem#

Eksepsi tetap berguna, tetapi tempatnya adalah batas sistem (boundary), yaitu lapisan yang bertatap muka dengan dunia luar. Penguraian masukan pengguna, pembacaan berkas, dan komunikasi jaringan adalah contoh lapisan semacam itu, karena kegagalannya bersumber dari luar kendali program. Pola yang rapi adalah menangkap eksepsi di lapisan itu saja, mengubahnya menjadi Either atau nilai cadangan, lalu menyebarkan hasilnya ke bagian dalam program sebagai data. Bagian dalam program kemudian bekerja dengan tipe yang menyatakan kegagalan secara eksplisit dan tidak perlu menyiapkan blok catch di setiap lapisan.

scala Contoh: konversi di batas sistem
def muatSkor(teks: String): Either[String, Int] =
  Try(teks.trim.toInt).toEither.left.map(_ => s"bukan bilangan: $teks")

Fragmen di atas memperlihatkan pola batas dalam satu fungsi, yaitu Try menangkap eksepsi dari toInt, toEither mengubahnya menjadi Either, dan left.map mengganti eksepsi mentah dengan pesan yang dimengerti pengguna. Setelah fungsi ini selesai, tidak ada eksepsi yang tersisa di nilai kembaliannya.

Menyebarkan eksepsi ke bagian dalam program juga menyulitkan pembacaan tanda tangan dan pengujian. Fungsi yang bertipe Int => Int dapat saja melempar NumberFormatException tanpa tanda apa pun pada tipenya, sedangkan fungsi yang mengembalikan Either menjanjikan bahwa kegagalan itu bagian dari nilai kembaliannya. Topik Option menangani kasus ketiadaan nilai, sedangkan Either menangani kasus kegagalan yang menyertakan alasan, dan keduanya membuat alur error terbaca pada tipe. Apabila lapisan penampilan kemudian membutuhkan teks, lapisan itulah yang memilih cara memunculkan pesannya, sehingga data dan penyajiannya tetap terpisah.

Ringkasan#

  • Try[A] mengubah operasi yang melempar eksepsi menjadi nilai Success atau Failure, dan recover memberi nilai cadangan pada cabang kegagalan.
  • Either[E, A] menyatakan kegagalan yang bermakna, dengan Left bertipe khusus berisi data error dan Right berisi hasil yang sah.
  • map mengubah isi Right, left.map menerjemahkan Left, dan fold menyatukan kedua cabang menjadi satu tipe keluaran.
  • Eksepsi ditempatkan pada batas sistem saja, yaitu penguraian masukan pengguna dan IO, dan hasilnya disebarkan ke dalam program sebagai data.

Referensi#