Ketika menggunakan lingkungan fungsional seperti Scala dan cats-effect
, haruskah konstruksi objek stateful dimodelkan dengan jenis efek?
// not a value/case class
class Service(s: name)
def withoutEffect(name: String): Service =
new Service(name)
def withEffect[F: Sync](name: String): F[Service] =
F.delay {
new Service(name)
}
Konstruksinya tidak bisa salah, jadi kami bisa menggunakan typeclass yang lebih lemah Apply
.
// never throws
def withWeakEffect[F: Applicative](name: String): F[Service] =
new Service(name).pure[F]
Saya kira semua ini murni dan deterministik. Hanya saja, tidak transparan secara referensial karena instance yang dihasilkan berbeda setiap kali. Apakah itu saat yang tepat untuk menggunakan tipe efek? Atau akankah ada pola fungsional yang berbeda di sini?
scala
functional-programming
scala-cats
cats-effect
Mark Canlas
sumber
sumber
delay
dan mengembalikan F [Layanan] . Sebagai contoh, lihatstart
metode pada IO , ia mengembalikan IO [Fiber [IO,?]] , Bukan serat polos .Jawaban:
Jika Anda sudah menggunakan sistem efek, kemungkinan besar ia memiliki
Ref
tipe untuk mengenkapsulasi keadaan yang dapat diubah dengan aman.Jadi saya katakan: model objek stateful dengan
Ref
. Karena pembuatan (dan juga akses) itu sudah berpengaruh, ini akan secara otomatis membuat layanan menjadi efektif juga.Ini dengan rapi memandu pertanyaan awal Anda.
Jika Anda ingin mengelola keadaan internal yang dapat berubah secara manual dengan teratur,
var
Anda harus memastikan sendiri bahwa semua operasi yang menyentuh status ini dianggap sebagai efek (dan kemungkinan besar juga dibuat aman untuk thread), yang membosankan dan rawan kesalahan. Ini bisa dilakukan, dan saya setuju dengan jawaban @ atl bahwa Anda tidak harus membuat penciptaan objek stateful menjadi efektif (selama Anda dapat hidup dengan hilangnya integritas referensial), tetapi mengapa tidak menyelamatkan diri dari masalah dan merangkul alat sistem efek Anda sepanjang jalan?Jika pertanyaan Anda dapat diulang menjadi
lalu: Ya, tentu saja .
Untuk memberikan contoh mengapa ini berguna:
Berikut ini berfungsi dengan baik, meskipun penciptaan layanan tidak berpengaruh:
Tetapi jika Anda memperbaiki ini seperti di bawah ini Anda tidak akan mendapatkan kesalahan waktu kompilasi, tetapi Anda akan mengubah perilaku dan kemungkinan besar telah memperkenalkan bug. Jika Anda telah menyatakan
makeService
efektif, refactoring tidak akan mengetik-periksa dan ditolak oleh kompiler.Memberi penamaan metode sebagai
makeService
(dan dengan parameter, juga) harus membuatnya cukup jelas apa yang dilakukan oleh metode tersebut dan bahwa refactoring bukanlah hal yang aman untuk dilakukan, tetapi "penalaran lokal" berarti Anda tidak perlu melihat pada konvensi penamaan dan implementasimakeService
untuk mencari tahu: Ekspresi apa pun yang tidak dapat secara mekanis diacak (diduplikasi, dibuat malas, dibuat bersemangat, kode mati dihilangkan, diparalelkan, ditunda, di-cache, dibersihkan dari cache, dll) tanpa mengubah perilaku ( yaitu tidak "murni") harus diketik sebagai efektif.sumber
Apa yang dimaksud dengan layanan stateful dalam kasus ini?
Apakah maksud Anda bahwa itu akan mengeksekusi efek samping ketika suatu objek dibangun? Untuk ini, ide yang lebih baik adalah memiliki metode yang menjalankan efek samping ketika aplikasi Anda mulai. Alih-alih menjalankannya selama konstruksi.
Atau mungkin Anda mengatakan bahwa ia memiliki status yang bisa berubah di dalam layanan? Selama keadaan bisa berubah internal tidak terkena, itu harus baik-baik saja. Anda hanya perlu memberikan metode murni (transparan referensial) untuk berkomunikasi dengan layanan.
Untuk memperluas poin kedua saya:
Katakanlah kita sedang membangun di memori db.
IMO, ini tidak perlu efektif, karena hal yang sama terjadi jika Anda melakukan panggilan jaringan. Meskipun, Anda perlu memastikan bahwa hanya ada satu instance dari kelas ini.
Jika Anda menggunakan
Ref
efek kucing, apa yang biasanya saya lakukan, adalah keflatMap
ref pada titik masuk, sehingga kelas Anda tidak harus efektif.OTOH, jika Anda menulis layanan bersama atau perpustakaan yang bergantung pada objek stateful (katakanlah beberapa concurrency primitif) dan Anda tidak ingin pengguna Anda peduli apa yang harus diinisialisasi.
Maka, ya, itu harus dibungkus dalam suatu efek. Anda dapat menggunakan sesuatu seperti,
Resource[F, MyStatefulService]
untuk memastikan bahwa semuanya sudah ditutup dengan benar. Atau hanyaF[MyStatefulService]
jika tidak ada yang ditutup.sumber
val neverRunningThisButStillMessingUpState = Task.pure(service.changeStateThinkingThisIsPure()).repeat(5)
)pure
adalah bahwa itu harus transparan secara referensi. mis. pertimbangkan contoh dengan Future.val x = Future {... }
dandef x = Future { ... }
berarti hal yang berbeda. (Ini dapat menggigit Anda ketika Anda refactoring kode Anda) Tapi, tidak demikian halnya dengan efek kucing, monix atau zio.