Kesadaran effect system

cats-effect dan ZIO sebagai langkah lanjutan menuju pemrograman berbasis efek di produksi.

Daftar isi

Program Scala yang siap untuk produksi hampir selalu berinteraksi dengan lingkungan di luar sistemnya, yaitu mencetak ke terminal, membaca berkas, memanggil jaringan, atau menunggu waktu. Tindakan-tindakan itu adalah efek samping (side effect), dan effect system adalah pendekatan yang mengelolanya secara eksplisit pada tingkat tipe. Topik ini memberi kesadaran atas dua effect system utama di ekosistem Scala, yaitu cats-effect dengan tipenya IO dan ZIO dengan tipenya ZIO, melalui contoh kecil yang benar-benar berjalan. Setelah topik ini dibaca, pola IO[A] dan ZIO[R, E, A] pada kode produksi dapat dikenali sebagai deskripsi program yang dieksekusi pada titik tertentu, dan sumber resmi untuk belajar lebih lanjut tersedia.

Masalah yang dijawab effect system#

Sebuah fungsi bertipe def kirim(s: String): Int hanya memperlihatkan hasil akhirnya, sedangkan efek samping yang dilakukannya di dalam tidak terlihat pada tipe. Pencetakan, akses berkas, dan panggilan jaringan dapat tersembunyi di mana saja, sehingga urutan eksekusi dan kegagalannya sulit disusun dengan aman. Future pada pustaka standar sudah menunjukkan sebagian jalan keluarnya, tetapi Future langsung dijadwalkan pada saat dibuat, sehingga pendefinisian dan eksekusinya menyatu dan tidak dapat dipisahkan. Effect system memisahkan keduanya dengan merepresentasikan efek sebagai nilai deskripsi, yaitu objek yang memuat rencana kerja tanpa menjalankannya.

Deskripsi itu kemudian dikomposisikan dengan map dan flatMap, sama seperti transformasi pada Option dan Either, hingga menjadi satu program utuh. Eksekusi baru terjadi di tepi program, misalnya pada titik masuk aplikasi, sehingga kode di antaranya memanipulasi nilai tanpa menjalankan apa pun. Persoalan kedua yang dijawab adalah keamanan sumber daya (resource safety), yaitu jaminan bahwa berkas yang dibuka tetap ditutup meskipun pemrosesannya gagal. Disiplin try dan finally yang ditulis manual mudah terlewat pada salah satu jalur kegagalan, sedangkan effect system menyediakan konstruksi khusus yang menutup sumber daya secara otomatis.

IO pada cats-effect sebagai deskripsi program#

cats-effect merepresentasikan deskripsi program dengan tipe IO[A], yaitu nilai yang memuat rencana kerja dan hasilnya bertipe A. Nilai run bertipe IO[Unit] pada contoh berikut menyusun tiga langkah, yaitu mencetak baris, menghitung jumlah, dan mencetak hasilnya, melalui for yang merupakan penulisan ringkas dari rangkaian flatMap dan map. Titik masuk programnya adalah IOApp.Simple, yaitu trait yang menyediakan runtime dan mengeksekusi run setelah program dimulai. Seluruh kode pada objek Jumlah hanya membangun deskripsi, karena IO(...) menyimpan bloknya untuk dieksekusi nanti.

scala jumlah.scala
//> using dep org.typelevel::cats-effect:3.6.3

// IO adalah deskripsi program, sedangkan IOApp.Simple yang mengeksekusinya
import cats.effect.IO
import cats.effect.IOApp

object Jumlah extends IOApp.Simple:
  // Nilai di bawah hanyalah deskripsi, belum ada yang dijalankan
  val deskripsi: IO[Int] = IO((1 to 100).sum)

  val run: IO[Unit] =
    for
      _     <- IO.println("Menjalankan deskripsi")
      hasil <- deskripsi
      _     <- IO.println(s"Jumlah 1 sampai 100: $hasil")
    yield ()
bash
# menjalankan program jumlah.scala
scala-cli run jumlah.scala
text Keluaran
Menjalankan deskripsi
Jumlah 1 sampai 100: 5050

Keluaran hanya berisi dua baris dari IO.println, dan tidak ada satu langkah pun yang dijalankan pada saat objek Jumlah didefinisikan. Apabila deskripsi tidak pernah disusun ke dalam run, penghitungannya tidak pernah terjadi. Nilai deskripsi yang sama juga dapat disusun ulang ke dalam deskripsi lain, dan setiap eksekusinya mengulang penghitungan dari awal, karena deskripsi tidak berubah setelah dibuat.

Jaminan penutupan dengan Resource#

Tipe Resource[IO, A] membungkus perolehan sumber daya beserta penutupnya, dan blok use menjamin penutupan itu terjadi pada setiap jalur, baik jalur sukses maupun jalur kegagalan. Contoh berikut membuka Source dari sebuah berkas dan menyerahkan penutupannya kepada Resource, sehingga tidak ada pemanggilan close yang ditulis manual. Apabila efek di dalam use gagal di tengah eksekusi, penutupan tetap dijalankan sebelum kegagalan dilaporkan ke pemanggil.

scala Contoh: Resource menjamin penutupan berkas
import cats.effect.{IO, Resource}
import scala.io.Source

def sumberData(nama: String): Resource[IO, Source] =
  Resource.fromAutoCloseable(IO.blocking(Source.fromFile(nama)))

val program: IO[Unit] =
  sumberData("data.txt").use(sumber =>
    IO.println(sumber.getLines().mkString(", "))
  )

Bentuk ini adalah versi terstruktur dari disiplin try dan finally, dengan penutupan yang disusun sekali dan digunakan untuk semua jalur. Konstruksi yang sama berlaku untuk sumber daya yang lebih rumit, misalnya koneksi jaringan yang dibagi ke beberapa pengguna, melalui penyusunan Resource yang bersarang. Kewajiban menelusuri setiap jalur kegagalan menjadi tidak relevan, karena jaminannya berada pada konstruksi, bukan pada kedisiplinan pemanggil.

ZIO dengan tiga kanal pada tipenya#

ZIO merepresentasikan efek dengan tipe ZIO[R, E, A] yang memiliki tiga kanal, yaitu lingkungan R yang dibutuhkan efek, error E yang mungkin terjadi, dan hasil A. Alias Task[A] mengunci R pada Any dan E pada Throwable, sehingga Task[A] setara dengan ZIO[Any, Throwable, A]. Titik masuk programnya adalah ZIOAppDefault, yang mengeksekusi nilai run dengan runtime milik ZIO. Contoh berikut membaca teks konfigurasi yang seharusnya berupa angka, dan kegagalan konversinya ditangani pada kanal error tanpa exception yang lolos.

scala konfigurasi.scala
//> using dep dev.zio::zio:2.1.27

// Task[A] adalah ZIO[Any, Throwable, A], efek dengan kanal error Throwable
import zio.*

object Konfigurasi extends ZIOAppDefault:
  val run: ZIO[Any, Throwable, Unit] =
    for
      _     <- Console.printLine("Memeriksa konfigurasi")
      angka <- ZIO.attempt("delapan".toInt).orElse(ZIO.succeed(0))
      _     <- Console.printLine(s"Nilai konfigurasi: $angka")
    yield ()
bash
# menjalankan program konfigurasi.scala
scala-cli run konfigurasi.scala
text Keluaran
Memeriksa konfigurasi
Nilai konfigurasi: 0

ZIO.attempt("delapan".toInt) mengubah kode yang dapat melempar exception menjadi efek dengan kanal error Throwable, dan orElse menggantikan hasil gagalnya dengan ZIO.succeed(0). Karena kanal error merupakan bagian dari tipe, kecenderungan sebuah fungsi untuk gagal terlihat langsung dari tanda tangannya. Metode Console.printLine pada contoh mengembalikan efek dengan kanal error IOException, yang terhitung sebagai Throwable, sehingga seluruh for tetap bertipe ZIO[Any, Throwable, Unit].

Aspekcats-effectZIO
Tipe efekIO[A]ZIO[R, E, A], dengan alias Task[A]
Kanal errortersedia melalui attemptbagian eksplisit dari tipe
Titik masuk programIOApp.SimpleZIOAppDefault
Runtimeruntime cats-effectruntime ZIO

Kesadaran, bukan penguasaan#

Topik ini memberikan kesadaran, bukan penguasaan. Kode produksi yang menggunakan cats-effect atau ZIO melibatkan hal-hal yang belum disentuh di sini, antara lain fiber sebagai unit konkurensi ringan, primitif bersama seperti Ref dan Queue, streaming, pengujian deterministik yang melibatkan waktu, dan pengelolaan skop yang bertingkat. Menguasainya membutuhkan dokumentasi resmi masing-masing proyek serta latihan pada proyek nyata. Yang dapat dibawa dari topik ini adalah kebiasaan membaca, yaitu mengenali bahwa IO[A] atau ZIO[R, E, A] adalah deskripsi yang belum berjalan, menelusuri susunan komposisinya, dan mencari titik eksekusinya di tepi program. Dengan kebiasaan itu, kode berbasis efek dapat dibaca secara terstruktur alih-alih ditebak baris demi baris.

Ringkasan#

  • Effect system merepresentasikan efek samping sebagai nilai deskripsi yang dikomposisikan, lalu dieksekusi sekali di tepi program.
  • IO[A] pada cats-effect adalah deskripsi yang dieksekusi runtime melalui IOApp.Simple, dan Resource.use menjamin penutupan sumber daya pada jalur sukses maupun gagal.
  • ZIO[R, E, A] menempatkan kanal error pada tipenya, dengan Task[A] sebagai alias dan ZIOAppDefault sebagai titik masuk.
  • Topik ini memberikan kesadaran pola, bukan penguasaan yang membutuhkan dokumentasi resmi dan latihan lebih lanjut.

Referensi#