Haruskah membangun objek stateful dimodelkan dengan tipe efek?

9

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?

Mark Canlas
sumber
2
Ya, penciptaan keadaan yang bisa berubah adalah efek samping. Dengan demikian, itu harus terjadi di dalam delaydan mengembalikan F [Layanan] . Sebagai contoh, lihat startmetode pada IO , ia mengembalikan IO [Fiber [IO,?]] , Bukan serat polos .
Luis Miguel Mejía Suárez
1
Untuk jawaban lengkap untuk masalah ini, silakan lihat ini & ini .
Luis Miguel Mejía Suárez

Jawaban:

3

Haruskah membangun objek stateful dimodelkan dengan tipe efek?

Jika Anda sudah menggunakan sistem efek, kemungkinan besar ia memiliki Reftipe untuk mengenkapsulasi keadaan yang dapat diubah dengan aman.

Jadi saya katakan: model objek stateful denganRef . 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, varAnda 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?


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?

Jika pertanyaan Anda dapat diulang menjadi

Apakah manfaat tambahan (di atas implementasi yang bekerja dengan benar menggunakan "kelas yang lebih lemah") dari transparansi referensial dan alasan lokal cukup untuk membenarkan menggunakan jenis efek (yang harus sudah digunakan untuk akses dan mutasi negara) juga untuk negara ciptaan?

lalu: Ya, tentu saja .

Untuk memberikan contoh mengapa ini berguna:

Berikut ini berfungsi dengan baik, meskipun penciptaan layanan tidak berpengaruh:

val service = makeService(name)
for {
  _ <- service.doX()
  _ <- service.doY()
} yield Ack.Done

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 makeServiceefektif, refactoring tidak akan mengetik-periksa dan ditolak oleh kompiler.

for {
  _ <- makeService(name).doX()
  _ <- makeService(name).doY()
} yield Ack.Done

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 implementasi makeServiceuntuk 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.

Thilo
sumber
2

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.

class InMemoryDB(private val hashMap: ConcurrentHashMap[String, String]) {
  def getId(s: String): IO[String] = ???
  def setId(s: String): IO[Unit] = ???
}

object InMemoryDB {
  def apply(hashMap: ConcurrentHashMap[String, String]) = new InMemoryDB(hashMap)
}

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 Refefek kucing, apa yang biasanya saya lakukan, adalah ke flatMapref pada titik masuk, sehingga kelas Anda tidak harus efektif.

object Effectful extends IOApp {

  class InMemoryDB(storage: Ref[IO, Map[String, String]]) {
    def getId(s: String): IO[String] = ???
    def setId(s: String): IO[Unit] = ???
  }

  override def run(args: List[String]): IO[ExitCode] = {
    for {
      storage <- Ref.of[IO, Map[String, String]](Map.empty[String, String])
      _ = app(storage)
    } yield ExitCode.Success
  }

  def app(storage: Ref[IO, Map[String, String]]): InMemoryDB = {
    new InMemoryDB(storage)
  }
}

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 hanya F[MyStatefulService]jika tidak ada yang ditutup.

atl
sumber
"Anda hanya perlu memberikan metode metode murni untuk berkomunikasi dengan layanan" Atau mungkin sebaliknya: Konstruksi awal keadaan internal murni tidak perlu menjadi efek, tetapi setiap operasi pada layanan yang berinteraksi dengan keadaan yang bisa berubah di cara apa pun perlu ditandai efektif (untuk menghindari kecelakaan seperti val neverRunningThisButStillMessingUpState = Task.pure(service.changeStateThinkingThisIsPure()).repeat(5))
Thilo
Atau datang dari sisi lain: Jika Anda membuat penciptaan layanan itu efektif atau tidak, tidak terlalu penting. Tetapi tidak peduli ke arah mana Anda pergi, berinteraksi dengan layanan itu dengan cara apa pun harus efektif (karena ia membawa keadaan yang bisa berubah di dalam yang akan dipengaruhi oleh interaksi ini).
Thilo
1
@ Thilo Ya, Anda benar. Yang saya maksud pureadalah bahwa itu harus transparan secara referensi. mis. pertimbangkan contoh dengan Future. val x = Future {... }dan def 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.
atl