Bagaimana memodelkan contoh ini
Bagaimana ini bisa dimodelkan dengan Reader monad?
Saya tidak yakin apakah ini harus dimodelkan dengan Pembaca, namun bisa juga dengan:
- mengkodekan kelas sebagai fungsi yang membuat permainan kode lebih bagus dengan Reader
- menyusun fungsi dengan Reader untuk pemahaman dan menggunakannya
Tepat sebelum memulai, saya perlu memberi tahu Anda tentang sedikit penyesuaian kode sampel yang menurut saya bermanfaat untuk jawaban ini. Perubahan pertama adalah tentang FindUsers.inactive
metode. Saya membiarkannya kembali List[String]
sehingga daftar alamat dapat digunakan dalam UserReminder.emailInactive
metode. Saya juga menambahkan implementasi sederhana ke metode. Terakhir, contoh akan menggunakan versi linting manual dari Reader monad:
case class Reader[Conf, T](read: Conf => T) { self =>
def map[U](convert: T => U): Reader[Conf, U] =
Reader(self.read andThen convert)
def flatMap[V](toReader: T => Reader[Conf, V]): Reader[Conf, V] =
Reader[Conf, V](conf => toReader(self.read(conf)).read(conf))
def local[BiggerConf](extractFrom: BiggerConf => Conf): Reader[BiggerConf, T] =
Reader[BiggerConf, T](extractFrom andThen self.read)
}
object Reader {
def pure[C, A](a: A): Reader[C, A] =
Reader(_ => a)
implicit def funToReader[Conf, A](read: Conf => A): Reader[Conf, A] =
Reader(read)
}
Langkah pemodelan 1. Mengkodekan kelas sebagai fungsi
Mungkin itu opsional, saya tidak yakin, tapi nanti itu membuat pemahaman for terlihat lebih baik. Perhatikan, fungsi yang dihasilkan adalah kari. Ini juga mengambil argumen konstruktor sebelumnya sebagai parameter pertama mereka (daftar parameter). Lewat situ
class Foo(dep: Dep) {
def bar(arg: Arg): Res = ???
}
menjadi
object Foo {
def bar: Dep => Arg => Res = ???
}
Perlu diingat bahwa masing-masing Dep
, Arg
, Res
jenis dapat benar-benar sewenang-wenang: tupel, fungsi atau tipe sederhana.
Berikut kode contoh setelah penyesuaian awal, diubah menjadi fungsi:
trait Datastore { def runQuery(query: String): List[String] }
trait EmailServer { def sendEmail(to: String, content: String): Unit }
object FindUsers {
def inactive: Datastore => () => List[String] =
dataStore => () => dataStore.runQuery("select inactive")
}
object UserReminder {
def emailInactive(inactive: () => List[String]): EmailServer => () => Unit =
emailServer => () => inactive().foreach(emailServer.sendEmail(_, "We miss you"))
}
object CustomerRelations {
def retainUsers(emailInactive: () => Unit): () => Unit =
() => {
println("emailing inactive users")
emailInactive()
}
}
Satu hal yang perlu diperhatikan di sini adalah bahwa fungsi tertentu tidak bergantung pada keseluruhan objek, tetapi hanya pada bagian yang digunakan secara langsung. Di mana dalam versi OOP UserReminder.emailInactive()
contoh akan memanggil di userFinder.inactive()
sini itu hanya panggilan inactive()
- sebuah fungsi yang diteruskan ke parameter pertama.
Harap dicatat, bahwa kode tersebut menunjukkan tiga properti yang diinginkan dari pertanyaan:
- jelas jenis dependensi yang dibutuhkan setiap fungsionalitas
- menyembunyikan ketergantungan dari satu fungsi dari yang lain
retainUsers
Metode seharusnya tidak perlu tahu tentang dependensi Datastore
Pemodelan langkah 2. Menggunakan Pembaca untuk membuat fungsi dan menjalankannya
Reader monad memungkinkan Anda hanya membuat fungsi yang semuanya bergantung pada jenis yang sama. Ini seringkali bukan kasus. Dalam contoh kita
FindUsers.inactive
bergantung pada Datastore
dan UserReminder.emailInactive
terus EmailServer
. Untuk mengatasi masalah itu, seseorang dapat memperkenalkan tipe baru (sering disebut sebagai Config) yang berisi semua dependensi, kemudian mengubah fungsinya sehingga semuanya bergantung padanya dan hanya mengambil data yang relevan darinya. Itu jelas salah dari perspektif manajemen ketergantungan karena dengan cara itu Anda membuat fungsi-fungsi ini juga bergantung pada jenis yang seharusnya tidak mereka ketahui sejak awal.
Untungnya, ternyata ada cara untuk membuat fungsi tersebut berfungsi Config
meskipun ia hanya menerima sebagian darinya sebagai parameter. Ini adalah metode yang disebut local
, didefinisikan di Reader. Itu perlu disediakan dengan cara untuk mengekstrak bagian yang relevan dari Config
.
Pengetahuan ini yang diterapkan pada contoh yang ada akan terlihat seperti ini:
object Main extends App {
case class Config(dataStore: Datastore, emailServer: EmailServer)
val config = Config(
new Datastore { def runQuery(query: String) = List("john.doe@fizzbuzz.com") },
new EmailServer { def sendEmail(to: String, content: String) = println(s"sending [$content] to $to") }
)
import Reader._
val reader = for {
getAddresses <- FindUsers.inactive.local[Config](_.dataStore)
emailInactive <- UserReminder.emailInactive(getAddresses).local[Config](_.emailServer)
retainUsers <- pure(CustomerRelations.retainUsers(emailInactive))
} yield retainUsers
reader.read(config)()
}
Keuntungan menggunakan parameter konstruktor
Dalam aspek apa menggunakan Reader Monad untuk "aplikasi bisnis" seperti itu lebih baik daripada hanya menggunakan parameter konstruktor?
Saya berharap bahwa dengan mempersiapkan jawaban ini saya mempermudah untuk menilai sendiri dalam aspek apa itu mengalahkan konstruktor biasa. Namun jika saya harus menghitungnya, inilah daftar saya. Penafian: Saya memiliki latar belakang OOP dan saya mungkin tidak menghargai Reader dan Kleisli sepenuhnya karena saya tidak menggunakannya.
- Keseragaman - tidak peduli seberapa pendek / panjang pemahaman for, ini hanya Reader dan Anda dapat dengan mudah menyusunnya dengan instance lain, mungkin hanya memperkenalkan satu jenis Config lagi dan menambahkan beberapa
local
panggilan di atasnya. Poin ini IMO lebih merupakan masalah selera, karena ketika Anda menggunakan konstruktor tidak ada yang mencegah Anda untuk membuat apa pun yang Anda suka, kecuali seseorang melakukan sesuatu yang bodoh, seperti melakukan pekerjaan di konstruktor yang dianggap praktik buruk di OOP.
- Pembaca adalah monad, jadi ia mendapat semua manfaat yang terkait dengan itu -
sequence
, traverse
metode yang diterapkan secara gratis.
- Dalam beberapa kasus, Anda mungkin merasa lebih baik untuk membangun Pembaca hanya sekali dan menggunakannya untuk berbagai Konfigurasi. Dengan konstruktor tidak ada yang menghalangi Anda untuk melakukan itu, Anda hanya perlu membangun seluruh grafik objek lagi untuk setiap Config yang masuk. Meskipun saya tidak memiliki masalah dengan itu (saya bahkan lebih suka melakukan itu pada setiap permintaan untuk melamar), itu bukan ide yang jelas bagi banyak orang karena alasan yang mungkin hanya saya spekulasi.
- Pembaca mendorong Anda untuk menggunakan lebih banyak fungsi, yang akan bermain lebih baik dengan aplikasi yang ditulis dalam gaya FP.
- Pembaca memisahkan perhatian; Anda dapat membuat, berinteraksi dengan semuanya, mendefinisikan logika tanpa memberikan dependensi. Sebenarnya suplai nanti, secara terpisah. (Terima kasih Ken Scrambler untuk poin ini). Ini sering terdengar keuntungan dari Reader, tetapi itu juga mungkin dengan konstruktor biasa.
Saya juga ingin memberi tahu apa yang tidak saya sukai di Pustaka.
- Pemasaran. Terkadang saya mendapat kesan, bahwa Reader dipasarkan untuk semua jenis dependensi, tanpa perbedaan apakah itu cookie sesi atau database. Bagi saya, tidak ada gunanya menggunakan Pustaka untuk objek yang praktis konstan, seperti server email atau repositori dari contoh ini. Untuk dependensi seperti itu, saya menemukan konstruktor biasa dan / atau fungsi yang diterapkan sebagian jauh lebih baik. Pada dasarnya Reader memberi Anda fleksibilitas sehingga Anda dapat menentukan dependensi Anda di setiap panggilan, tetapi jika Anda tidak benar-benar membutuhkannya, Anda hanya membayar pajaknya.
- Bobot tersirat - menggunakan Pustaka tanpa implikasi akan membuat contoh sulit dibaca. Di sisi lain, ketika Anda menyembunyikan bagian yang berisik menggunakan implik dan membuat beberapa kesalahan, kompiler terkadang akan memberi Anda pesan yang sulit diuraikan.
- Upacara dengan
pure
, local
dan membuat kelas Config sendiri / menggunakan tupel untuk itu. Pembaca memaksa Anda untuk menambahkan beberapa kode yang bukan tentang domain masalah, oleh karena itu menimbulkan beberapa gangguan dalam kode. Di sisi lain, aplikasi yang menggunakan konstruktor seringkali menggunakan pola pabrik, yang juga berasal dari luar domain masalah, sehingga kelemahan ini tidak terlalu serius.
Bagaimana jika saya tidak ingin mengonversi kelas saya menjadi objek dengan fungsi?
Kamu ingin. Anda secara teknis dapat menghindari itu, tetapi lihat saja apa yang akan terjadi jika saya tidak mengonversi FindUsers
kelas ke objek. Baris masing-masing untuk pemahaman akan terlihat seperti:
getAddresses <- ((ds: Datastore) => new FindUsers(ds).inactive _).local[Config](_.dataStore)
mana yang tidak bisa dibaca, bukan? Intinya adalah Pembaca beroperasi pada fungsi, jadi jika Anda belum memilikinya, Anda perlu membuatnya sebaris, yang seringkali tidak terlalu bagus.