Daftar Isi
- Mengenai Buku Ini
- Pemberitahuan Copyleft
- Ucapan Terima Kasih
- Kepraktisan
- 1. Pengantar
- 2. Komprehensi For
- 3. Desain Aplikasi
- 4. Data dan Fungsionalitas
- 5. Kelas Tipe Scalaz
- 6. Tipe Data Scalaz
- 7. Monad Lanjutan
- 8. Derivasi Kelas Tipe
- 9. Merangkai Aplikasi
- Contekan Kelas Tipe
- Haskell
- Lisensi Pihak Ketiga
“Love is wise; hatred is foolish. In this world, which is getting more and more closely interconnected, we have to learn to tolerate each other, we have to learn to put up with the fact that some people say things that we don’t like. We can only live together in that way. But if we are to live together, and not die together, we must learn a kind of charity and a kind of tolerance, which is absolutely vital to the continuation of human life on this planet.”
― Bertrand Russell
Mengenai Buku Ini
Buku ini ditujukan untuk tipikal pengembang yang menggunakan bahasa pemrograman Scala, yang mungkin memiliki latar belakang Java, yang skeptis dan penasaran mengenai paradigma Pemrograman Fungsional (PF). Buku ini menyuguhkan setiap konsep dengan contoh praktikan, termasuk dengan penulisan aplikasi web.
Buku ini menggunakan Scalaz 7.2 yang merupakan kerangka kerja Pemrograman Fungsional untuk Scala yang paling populer, stabil, berprinspip, dan komprehensif.
Buku ini dirancang agar dibaca dari awal sampai akhir secara berurutan, dengan rehat sejenak antar bab. Pada bab awal, pembaca budiman didorong untuk menggunakan gaya penulisan kode yang pada bab selanjutnya akan kita tinggalkan: mirip saat kita mempelajari teori gravitasi Newton saat masih kanak-kanak, dan berlanjut ke Riemann / Einstein / Maxwell bila kita menjadi mahasiswa Fisika.
Untuk mengikuti buku ini, sebuah komputer tidak diharuskan, namun didorong untuk mempelajari kode sumber Scalaz. Beberapa potongan kode yang agak kompleks tersedia bersama dengan kode sumber buku ini dan bagi pembaca budiman yang menginginkan latihan praktik, sangat dianjurkan untuk mengimplementasi ulang Scalaz (dan contoh aplikasi) menggunakan deskripsi parsial yang ditunjukkan di buku ini. (dan contoh aplikasi)
Kita juga merekomendasikan Buku Merah sebagai bacaan lainnya. Buku tersebut membimbing pembaca mengenai bagaimana cara membangun pustaka PF pada Scala dari prinsip awal.
Pemberitahuan Copyleft
Buku ini Libre dan mengikuti filosofi Perangkat Lunak Bebas: Pembaca dapat menggunakan buku ini sebagaimana yang pembaca suka, sumber buku dapat pembaca distribusikan ulang, mengirimkannya melalui surel, mengunggahnya pada situs web, mengubahnya, menerjemahkannya, meminta bayaran atasnya, menggabungkannya dengan bahan lain, menghapus bagian-bagiannya, dan bahkan menggambarinya.
Buku ini bersifat Copyleft: bila pembaca budiman mengubah buku ini dan mendistribusikannya, pembaca juga harus memberikan kebabasan ini kepada pembacanya.
Buku ini menggunakan lisensi Atribusi-BerbagiSerupa 4.0 Internasional (CC BY-SA 4.0).
Semua potongan kode pada buku ini dilisensikan terpisah menggunakan CC0, pembaca dapat menggunakannya tanpa batas. Kutipan dari Scalaz dan pustaka terkait tetap menggunakan lisensinya, dan dicantumkan pada lampiran.
Contoh aplikasi drone-dynamic-agents
didistribusikan menggunakan GPLv3:
hanya yang tercantum pada buku ini tersedia tanpa batasan.
Ucapan Terima Kasih
Diego Esteban Alonso Blas, Raúl Raja Martínez dan Peter Neyens dari 47 degrees, Rúnar Bjarnason, Tony Morris, John de Goes dan Edward Kmett atas bantuannya dalam menjelaskan prinsip PF. Kenji SHinoda dan Jason Zaugg sebagai penulis utama Scalaz, dan Paul Chiusano / Miles Sabin untuk pembenahan kutu ganas (SI-2712) pada kompiler Scala.
Terima kasih kepada pembaca yang memberikan umpan balik pada draf awal buku ini.
Beberapa materi yang berguna bagi pemahaman penulis atas konsep-konsep pada buku ini. Terima kasih kepada Juan Manuel Serrano untuk All Roads Lead to Lambda, Pere Villega untuk On Free Monads, Dick Wall dan Josh Suereth untuk For: What is it Good For?, Erik Bakker untuk Options in Futures, how to unsuck them, Noel Markham untuk ADTs for the Win!, Sukant Hajra untuk Classy Monad Transformers, Luka Jacobowitz untuk Optimizing Tagless Final, Vincent Marquez untuk Index your State, Gabriel Gonzalez untuk The Continuation Monad, dan Yi Lin Wei / Zainab Ali atas tutorial pada pertemuan di Hack The Tower.
Jiwa-jiwa penolong yang menjelaskan dengan sabar kepada penulis: Merlin Göttlinger, Edmund Noble, Fabio Labella, Adelbert Chang, Michael Pilquist, Paul Snively, Daniel Spiewak, Stephen Compall, Brian McKenna, Ryan Delucchi, Pedro Rodriguez, Emily Pillmore, Aaron Vargo, Tomas Mikula, Jean-Baptiste Giraudeau, Itamar Ravid, Ross A. Baker, Alexander Konovalov, Harrison Houghton, Alexandre Archambault, Christopher Davenport, Jose Cardona.
Kepraktisan
Untuk memulai sebuah projek yang menggunakan pustaka-pustaka yang ditunjukkan
pada buku ini, gunakan versi baru dari Scala dengan fitur spesifik PF diizinkan
(mis., pada build.sbt
):
Agar potongan kode kita tetap pendek, kita tidak akan mengikutsertakan bagian
import
. Kecuali bila ditentukan selainnya, anggap semua potongan memiliki
impor berikut:
1. Pengantar
Sudah menjadi naluri manusia untuk ragu dan curiga pada paradigma baru. Sebagai perspektif mengenai betapa berubahnya kita, dan pergeseran yang sudah kita terima pada JVM, mari kita rekap cepat apa yang terjadi pada 20 tahun belakangan ini.
Java 1.2 memperkenalkan APA Koleksi, yang memperkenankan kita untuk menulis metoda yang mengabstraksi koleksi tak tetap. Antarmuka ini sangat berguna untuk penulisan algoritma umum dan merupakan pondasi dari basis kode kita.
Namun ada sebuah masalah, kita harus melakukan konversi tipe pada saat waktu-jalan:
Menyikapi hal itu, pengembang mendefinisikan objek domain pada logika bisnis
mereka dan disebut sebagai CollectionOfThings
. Setelah itu, APA Koleksi
menjadi detail implementasi.
Pada tahun 2005, Java 5 memperkenalkan generik, yang memperkenankan kita
untuk mendefinisikan Collection<Thing>
, mengabstrakkan kontainer dan
elemennya. Generik mengubah cara kita menulis Java.
Penulis dari kompiler generik Java, Martin Odersky, lalu menciptakan Scala dengan sistem tipe yang lebih kuat, data tak-ubah, dan pewarisan jamak. Bahasa ini membawa penggabungan antara pemrograman berorientasi objek dan pemrograman fungsional.
Bagi kebanyakan pengembang, PF mempunyai makna penggunaan data tak-ubah
sebanyak mungkin. Namun, keadaan tak-tetak masih menjadi kebatilan yang
harus ada dan juga harus diisolasi dan dikekang. Misal, dengan aktor
Akka atau kelas synchronized
. Gaya PF semacam ini menghasilkan program
yang lebih sederhana dan dapat dengan mudah diparalelisasi dan distribusi.
Dengan kata lain, peningkatan atas Java. Namun, Gaya semacam ini hanya
berkutat pada permukaan dari keuntungan PF, yang akan kita temukan pada
buku ini.
Scala juga memiliki Future
, yang mempermudah penulisan aplikasi
asinkronus. Namun, ketika Future
menjadi tipe kembalian, semua
harus ditulis ulang agar mengakomodasinya. Termasuk tes yang harus
tunduk pada tenggat waktu arbiter.
Sekarang, kita memiliki masalah yang sama dengan Java 1.0: tidak ada cara untuk mengabstraksi eksekusi, sebagaimana kita tidak punya cara untuk mengabstraksi koleksi.
1.1 Abstraksi atas Eksekusi
Misalkan kita ingin berinteraksi dengan pengguna melalui antarmuka baris perinta.
Kita dapat membaca (menggunakan metoda read
) apa yang pengguna tulis dan kita
juga dapat menulis (menggunakan metoda write
) pesan kepada mereka.
Bagaimana kita menulis kode generik yang dapat menggemakan masukan pengguna secara sinkronus maupun asinkronus tergantung pada implementasi waktu-jalan kita?
Kita dapat menulis versi sinkronus dan melapisinya dengan Future
. Namun,
kita harus memikirkan kumpulan utas mana yang harus kita gunakan untuk
tugas ini, atau kita dapat menggunakan Await.result
untuk menanti yang
terjadi pada Future
dan memperkenalkan penghalangan utas. Yang manapun juga,
akan sangat banyak plat cetak yang digunakan dan kita berurusan dengan APA
yang berbeda secara mendasar dan juga tidak selaras.
Kita juga dapat menyelesaikan masalah, sebagaimana Java 1.2, menggunakan induk yang sama dengan memakai fitur bahasa milik Scala, tipe lebih tinggi (TLT).
Kita ingin mendefinisikan Terminal
untuk konstruktor tipe C[_]
. Dengan
mendefinisikan Now
untuk mengkonstruk parameter tipenya (seperti Id
), kita
dapat menngimplementasika antarmuka umum untuk terminal sinkronus dan asinkronus:
We can think of C
as a Context because we say “in the context of
executing Now
” or “in the Future
”.
Kita dapat menganggap C
sebagai konteks karena kita menggunakannya
pada saat berbicara sebagai “pada konteks eksekusi saat ini (Now
)” atau
“di masa depan (Future
)”.
Namun, kita tidak tahu apapun mengenai C
dan kita tidak dapat melakukan
apapun dengan C[String]
. Apa yang kita butuhkan adalah semacam lingkungan
eksekusi yang memperkenankan kita untuk memanggil metoda yang mengembalikan
sebuah C[T]
dan pada akhirnya dapat melakukan sesuatu pada T
, termasuk
memanggil metoda lain pada Terminal
. Kita juga membutuhkan sebuah cara untuk
membungkus sebuah nilai menjadi sebuah C[_]
. Penanda seperti ini bisa
dibilang bekerja dengan baik, sebagaimana apa yang kita butuhkan pada kalimat
sebelumnya:
yang memperkenankan kita untuk menulis:
Sekarang kita dapat berbagi implementasi echo
antara alur kode sinkronus
dan asinkronus. Kita juga dapat menulis implementasi tiruan untuk Terminal[Now]
dan menggunakannya pada tes kita tanpa batas waktu.
Implementasi dari Execution[Now]
dan Execution[Future]
dapat digunakan
oleh metoda generik seperti echo
.
Namun, kode untuk echo
sendiri cukup mengerikan.
Fitur bahasa Scala implicit class
memberikan C
beberapa metoda.
Kita akan memanggil metoda ini flatMap
dan map
untuk alasan yang
akan jelas sebentar lagi. Setiap metoda menerima sebuah implicit Execution[C]
,
namun hal ini tidak lebih daripada flatMap
dan map
yang kita gunakan pada
Seq
, Option
, dan Future
Sekarang kita tahu mengapa kita menggunakan flatMap
sebagai nama metoda:
metoda ini memperkenankan kita untuk menggunakan komprehensi for yang
hanya merupakan pemanis sintaks atas flatMap
dan map
berlapis.
Execution
kita mempunyai penanda yang sama sebagaimana dengan trait pada
Scalaz yang disebut Monad
. Namun, chain
adalah bind
dan create
adalah
pure
. Kita menganggap C
bersifat monad bila ada Monad[C]
implisit
tersedia. Sebagai tambahan Scalaz mempunyai alias tipe Id
.
Yang bisa diambil adalah: bila kita menulis metoda yang beroperasi pada
tipe monadik, maka kita dapat menulis kode sekuensial yang mengabstraksi
konteks eksekusinya. Disini, kita telah menunjukkan sebuah abstraksi atas
eksekusi sinkronus dan asinkronus. Namun, hal yang sama juga bisa digunakan
untuk penanganan galat yang lebih teliti (dimana C[_]
berupa Either[Error, _]
),
manajemen akses pada keadaan volatil, melakukan I/O, atau mengaudit sesi.
1.2 Pemrograman Fungsional Murni
Pemrograman fungsional adalah perilaku penulisan kode dengan fungsi murni. Fungsi murni memiliki tiga properti:
- Total: mengembalikan sebuah nilai untuk setiap masukan yang mungkin
- Deterministik: mengembalikan nilai yang sama untuk masukan yang sama
- Rabak: tak ada interaksi (langsung) dengan dunia luar atau keadaan program
Ketiga properti ini memberikan kita kemampuan yang belum pernah kita miliki untuk menalar kode kita. Sebagai contoh, validasi masukan akan lebih mudah bila kita mengisolasi dengan totalitas, penyimpanan ke tembolok mungkin bila fungsi adalah fungsi deterministik, dan interaksi dengan dunia luar lebih mudah diatur dan dites bila fungsi tak berhubungan langsung dunia luar.
Sesuatu yang merusak properti ini disebut efek samping: akses langsung atau
pengubahan keadaan tak tetap (mis., memelihara sebuah var
pada kelas atau
menggunakan APA peninggalan yang tidak murni), berkomunikasi dengan sumber daya
eksternal (mis. pencarian berkas atau jaringan), dan pelemparan dan penangkapan
eksepsi.
Kita menulis fungsi murni dengan menghindari eksepsi dan berinteraksi dengan dunia
luar hanya melalui sebuah konteks eksekusi F[_]
yang aman.
Pada bagian sebelumnya, kita mengabstrakkan eksekusi dan mendefinisikan echo[Id]
dan echo[Future]
. Kita mungkin berharap bahwa pemanggila echo
tidak akan
menyebabkan efek samping apapun, karena fungsi ini murni. Namun, bila kita
menggunakan Future
atau Id
sebagai konteks eksekusi, aplikasi kita akan
mulai mendengarkan stdin:
Kita telah merusak kemurnian eksekusi dan tidak lagi menulis kode PF: futureEcho
merupakan hasil dari penjalanan echo
sekali. Future
mengurung definisi program
dengan menafsirkannya (menjalankannya). Dan hasilnya, aplikasi yang dibangun
dengan Future
akan menyulitkan penalaran.
Kita dapat mendefinisikan sebuah konteks eksekusi yang aman, F[_]
yang dievaluasi secara luntung. IO
hanyalah sebuah struktur data yang merujuk
pada kode (yang mungkin) tak-murni, dan sebenarnya tidak menjalankan apapun.
Kita dapat mengimplementasikan Terminal[IO]
dan memanggil echo[IO]
agar dapat mendapatkan kembali sebuah nilai
{lang=”text”}
val delayed: IO[String] = echo[IO]
val delayed
dapat digunakan ulang karena ini hanya merupaka definisi dari
tugas yang harus diselesaikan. Kita dapat memetakan String
dan menyusun
program tambahan, sebagaimana kita dapat memetakan sebuah Future
. IO
memaksa kita untuk tetap jujur bahwa kita bergantung pada interaksi dengan
dunia luar, namun tidak mencegah kita untuk mengakses keluaran dari interaksi
tersebut.
Kode tak-murni didalam IO
hanya akan dievaluasi bila kita menafsirkan nilainya
dengan memanggil .interpret()
, yang merupakan tindakan tak-murni
Sebuah aplikasi yang tersusun dari program IO
hanya ditafsirkan satu kali,
pada metoda main
yang juga disebut sebagai ujung dunia.
Pada buku ini, kita memperluas konsep yang diperkenalkan pada bab ini dan menunjukkan bagaimana cara menulis fungsi murni dan dapat dipelihara yang mampun mencapai tujuan bisnis kita.
2. Komprehensi For
Komprehensi for
pada Scala merupakan abstraksi ideal pada pemrograman fungsional
untuk program-program yang berjalan secara berurutan serta berinteraksi dengan dunia luar.
Lebih lanjut, dikarenakan kita akan menggunakan kata kunci ini secara
intensif, kita akan mempelajari ulang prinsip for
dan bagaimana
Scalaz membantu kita untuk menulis kode yang lebih bersih.
Bab ini tidak akan membahas bagaimana cara menulis program murni dan teknik teknik yang bisa diterapkan di basis kode non-PF
2.1 Pemanis Sintaksis
Pada dasarnya, for
pada Scala hanya merupakan aturan penulisan
ulang sederhana, atau pemanis sintaksis, yang tidak memiliki
informasi kontekstual.
Untuk melihat apa yang terjadi pada for
, kita akan menggunakan fitur
show
dan reify
pada REPL untuk mencetak bentuk kode setelah pendugaan
tipe.
Sebagaimana yang terlihat pada potongan kode diatas, terdapat banyak
derau yang disebabkan oleh pemanis sintaksis seperti +
menjadi $plus
.
Selain itu, supaya ringkas dan terfokus, kita akan mengabaikan
show
dan reify
saat baris REPL berupa reify>
dan juga akan
merapikan hasil kode secara manual.
Yang menjadi patokan adalah, setiap <-
, biasa disebut generator, merupakan
eksekusi flatMap
yang bisa jadi berisi flatMap
lain, dengan
generator akhir berupa map
yang berisi konstruk yield
.
2.1.1 Penetapan Nilai
Pada for
, kita bisa membuat atau menetapkan sebuah nilai tanpa harus
secara spesifik menggunakan val
.
Dengan kata lain, kita bisa langsung menuliskan ij = i + j
sebagaimana
pada potongan kode berikut.
Pada hasil REPL di potongan diatas, selain munculnya j
, hasil dari
pemetaan (dengan .map
) b
, juga muncul ij
yang merupakan hasil dari
operasi i + j
. Kedua nilai diatas, j
dan ij
, akan dipetakan menggunakan
kode pada yield
.
Sayangnya, kita tidak dapat melakukan penetapan nilai sebelum generator. Walau belum diterapkan, hal ini sudah dibicarakan pada: https://github.com/scala/bug/issues/907
Untuk menyiasatinya kita bisa membuat val
di luar for
atau membuat Option
sebagai assignment pertama.
2.1.2 Filter
Bisa juga bila kita menggunakan pernyataan if
setelah generator
untuk menyaring nilai berdasarkan predikat tertentu.
Dahulu kala, Scala menggunakan filter
. Namun, dikarenakan Traversable.filter
selalu membuat koleksi objek baru untuk setiap predikat, dibuatlah withFilter
sebagai alternatif.
Patut diperhatikan, kita juga bisa secara tanpa sengaja menggunakan withFilter
dengan menambahkan informasi mengenai tipe.
Alasannya, informasi tersebut digunakan untuk case pencocokan pola.
Sebagaimana penetapan nilai, generator bisa menggunakan pencocokan pola pada persamaan
bagian kiri. Namun berbeda dengan assignment, yang melempar MatchError
saat terjadi
galat, generator akan menyaring operasi tersebut sehingga akan terhindar dari galat.
2.1.3 For Each
Bila tidak ditemukan yield
, kompilator akan menggunakan foreach
sebagai pengganti flatMap
.
2.1.4 Rangkuman
Tidak ada tipe super umum yang mempunyai metoda umum yang digunakan pada
for
; setiap potongan dikompilasi tersendiri.
Misalkan, ada trait
umum, kurang lebih akan terlihat sebagai berikut:
Adalah mu’bah bila konteks (C[_]
) dari for
tidak menyediakan map
dan flatMap
atau metoda lainnya. Jika scalaz.Bind[T]
tersedia untuk T
,
bind
tersebut akan menyediakan apa yang konteks tadi tidak miliki.
2.2 Senam
Walaupun penulisan kode berurutan untuk komprehensi for
mudah,
kadang terjadi hal hal yang menyebabkan kita berpikir keras. Bagian
ini berisi contoh-contoh mengenai hal semacam itu dan bagaimana cara kita
menyiasatinya.
2.2.1 Logika Cadangan
Anggap kata kita memanggil sebuah metoda yang mengembalikan Option
.
Bila pemanggilan ini gagal, tentu kita ingin ada metoda lain yang menangani
galat tersebut. Seperti saat kita membaca tembolok
Bilamana kita harus munggunakan versi asinkronus dari antarmuka pemrograman aplikasi,
maka kita harus hati hati betul agar jangan sampai menambah pekerjaan karena
will run both queries. We can pattern match on the first result but the type is wrong
akan menjalankan kedua kueri secara bersamaan. Kita dapat mencocokkan pola pada hasil pertama namun tipe hasil tersebut salah
Kita harus membuat Future
dari cache
Future.successful
membuat objek Future
baru, sebagaimana konstruktor
Option
maupun List
.
2.2.2 Pulang Duluan
Misalkan kita punya sebuah keadaan dimana harus selesai di tengah tengah dan mengembalikan nilai sukses.
Standar praktik pada OOP ketika kita harus keluar dari komputasi lebih awal adalah dengan melempar eksepsi
Yang dapat ditulas ulang secara asinkronus.
Namun, bila kita ingin keluar lebih awal dari komputasi dengan nilai yang ok, kode sinkronus yang sederhana semacam ini:
ketika diterjemahkan menjadi komprehensi for
berlapis saat kode
tersebut mempunyai ketergantungan asinkronus:
2.3 Jalan Penuh Derita
Sampai saat ini, kita baru membahas ingat mengenai aturaan penulisan ulang
dan belum membahas mengenai map
dan flatMap
.
Kadang-kadang, ada kondisi dimana for
harus berhenti di tengah-tengah. Apa
yang terjadi?
Pada contoh Option
, yield
hanya dipanggil jika dan hanya jika i, j, k
berhasil terdefinisi.
Misalkan salah satu dari a, b, c
adalah None
, akan terjadi hubungan pendek
pada komprehensi tersebut dan nilai None
akan dikembalikan tanpa memberikan
konteks tentang nilai mana yang berupa None
.
Di sisi lain, bila kita menggunakan Either
, seperti None
, Left
akan
menyebabkan arus-pendek namun memberikan informasi tambahan. Dengan demikian,
Either
merupakan pilihan yang jauh lebih baik daripada Option
.
Mari kita lihat apa yang terjadi bila Future
gagal:
Future
yang bertugas untuk mencetak ke terminal tidak akan pernah dipanggil
sebagaimana Option
dan Either
dikarenakan for
selesai lebih awal.
Penggunaan fungsi-arus-pendek adalah hal yang jamak dilakukan, penting
malah, pada alur kejadian yang tidak menyenangkan.
Hal yang juga patut diperhatikan adalah komprehensi for
tidak dapat
melakukan melepas sumber daya yang disebabkan tidak ada try
maupun
finally
.
Secara prinsip, pemrograman fungsional memancang dengan jelas siapa yang
bertanggung jawab ketika terjadi galat yang tak terduga.
Kewajiban tersebut jatuh kepada konteks eksekusi program, yang biasanya berupa
Monad
, bukan logika bisnis.
2.4 Ngelantur
Adalah haram untuk mencampur-adukkan konteks saat menggunakan komprehensi for
seperti pada cuplikan di bawah.
Bahkan, ketika kita kode yang ada di depan kita memiliki konteks berlapis, kompilator acap kali tidak paham maksud kode tersebut. Walau maksud dari kode tersebut terlihat jelas bagi kita.
Di atas, kita bermaksud agar for
mengurus mengenai konteks yang
melapisi Option
di dalam namun apa yang terjadi? Sesuai yang diduga
kompilator gagal menerka maksud kita.
Yang kita lakukan diatas, menghiraukan konteks bagian luar, biasa
dicapai dengan menggunakan transformator monad yang oleh Scalaz
disediakan implementasi untuk Option
dan Either
dengan nama
OptionT
dan EitherT
.
Pada dasarnya, apapun yang bisa digunakan pada komprehensi for
bisa digunakan sebagain konteks bagian luar, selama konsisten
sepanjang komprehensi tersebut.
Kita juga bisa menggunakan OptionT
untuk mengubah konteks for
dari
Future[Option[_]]
menjadi OptionT[Future, _]
yang ditunjukkan
pada REPL di bawah.
Dan dengan memanggil .run
, konteks yang semula berubah akan kembali
muncul.
Selain itu, transformator moda juga memperkenankan kita untuk menggunakan
Future[Option[_]]
dengan metoda-metoda yang hanya mengembalikan nilai
Future
saja melalui .liftM[OptionT]
yang disediakan oleh Scalaz.
Untuk lebih jelasnya, silakan simak contoh di bawah:
terlebih lagi, kita dapat mencampur penggunaan metoda yang mengembalikan
Option
dengan melapisinya dengan Future.successful
(.pure[Future]
,
bila menggunakan Scalaz) dan disambung dengan OptionT
Sudah barang tentu dengan mencampur banyak konteks akan menghasilkan
kode yang “berisik”. Akan tetapi, hal ini jauh lebih baik bila dibandingkan
dengan menulis flatMap
dan map
berlapis secara manual.
Selain itu, kita juga bisa membersihkannya dengan DSL yang menangani
pengubahan-pengubahan yang dibutuhkan agar menjadi OptionT[Future, _]
.
Ditambah lagi, dengan menggunakan operator |>
yang melewatkan nilai
di sebelah kiri ke fungsi di sebelah kanan operator tersebut, pembatasan
antara logika bisnis dengan transformator monad akan terlihat lebih jelas.
Pendekatan ini juga bisa digunakan untuk Either
dan transformator lainnya
sebagai konteks bagian dalam. Namun, metoda pengangkatan transformator
lebih kompleks dan membutuhkan parameter tambahan.
Scalaz menyediakan monad transformator bagi kebanyakan tipe yang dimilikinya.
Silakan periksa bila ada.
3. Desain Aplikasi
Pada bab ini, kita akan menulis logika bisnis dan tes tes untuk aplikasi
server fungsional murni. Kode sumber untuk aplikasi ini
bisa dilihat pada direktori example
bersama kode sumber buku ini.
Namun, sangat disarankan untuk tidak membacanya sampai kita sampai di bab akhir
dikarenakan akan ada refaktorisasi sepanjang pembelajaran kita tentang PF.
3.1 Spesifikasi
Aplikasi kita akan mengurus kompilasi-tepat-waktu “build farm” dengan pendanaan yang sangat mepet. Aplikasi ini akan memperhatikan ke sebuah server integrasi berkelanjutan Drone dan akan menelurkan “worker agent” menggunakan Google Container Engine (GKE) untuk memenuhi permintaan dari antrian kerja.
Drone menerima kerja ketika kontributor membuat sebuah permintaan tarik pada github ke proyek yang dimanage. Drone menetapkan beban kerja ke agen-agennya, dan pada akhirnya, tiap agen akan memproses satu tugas pada satu waktu.
Tujuan dari aplikasi kita adalah memastikan bahwa agen-agen akan selalu cukup untuk menyelesaikan tugas, dengan batasan-batasan pada jumlah agen dan pada saat yang bersamaan, menekan biaya keseluruhan. Aplikasi ini harus tahu jumlah barang yang ada pada backlog dan jumlah agen yang tersedia.
Google dapat menelurkan banyak simpul yang masing masing mampu mempunyai beberapa agen drone. Ketika sebuah agen mulai nyala, agen tersebut akan memberitahu kepada drone yang pada akhirnya mengambil alih siklus hidup (termasuk panggilan “keep-alive” untuk mendeteksi agen yang telah mati)
GKE menarik biaya berdasarkan uptime dalam hitungan menit, dibulatkan keatas ke jam terdekat untuk tiap node. Maka dari itu, kita tidak bisa secara sembarangan untuk menyalakan node baru untuk tiap tugas di antrian kerja. Kita harus menggunakan ulang node dan memaksa mereka untuk bekerja sampai sampai ke menit 58 agar tetap ekonomis.
Aplikasi kita harus bisa memulai dan mematikan simpul dan juga memeriksa status mereka, seperti waktu nyala dan daftar node yang tidak aktif, dan memastikan waktu saat ini menurut GKE.
Sebagai tambahan, tidak ada API yang secara langsung berkomunikasi ke sebuah agen yang mengakibatkan kita tidak dapat secara tahu secara langsung apakah sebuah agen sedang melakukan sesuatu untuk drone server atau tidak. Bila kita mematikan sebuah agen tanpa memastikan bahwa agen tersebut sedang menganggur, bisa jadi si agen tadi mati di tengah medan. Tentu sungguh menyesakkan bila harus memulai ulang agen tersebut secara manual.
Kontributor juga bisa menambahkan agen ke farm secara manual, sehingga menghitung agen tidak selalu sama dengan node. Kita tidak perlu menambah node bila ada agen yang tersedia.
Mode gagal harus selalu diambil sebagai opsi paling murah.
Drone dan GKE keduanya mempunya antarmuka JSON yang menggunakan antarmuka REST dengan otentikasi OAuth 2.0.
3.2 Antarmuka / Aljabar
Pada bab ini, kita akan mengkodifikasi diagram arsitektur pada bagian sebelumnya. Pertama, karena pada pustaka standar Java maupun Scala tidak memiliki tipe data timestamp kita akan membuat sebuah tipe data sederhana untuk keperluan ini.
Pada PF, sebuah aljabar mempunyai kedudukan yang sama dengan interface
di Java yang kurang lebih juga sama dengan pesan-pesan yang dianggap valid
oleh Actor
Akka. Aljabar ini pula-lah dimana kita mendefinisikan semua
interaksi yang mempunyai efek samping pada sistem kita.
Pada proses desain sistem, kita akan sering melakukan iterasi yang padat saat menulis logika bisnis dan aljabarnya: hal semacam ini merupakan tingkat abstraksi yang bagus untuk mendesain sebuah sistem.
Di sini, kita menggunakan NonEmptyList
yang dibuat dengan memanggil .toNel
pada tipe data List
yang ada pada pustaka standar.
Walaupun nilai yang dikembalikan adalah Option[NonEmptyList]
(karena List
bisa saja kosong), hal hal lain tidak berubah.
3.3 Logika Bisnis
Sekarang, kita akan menulis logika bisnis yang menentukan perilaku dari aplikasi ini, yang saat ini tidak mengindahkan sumber sumber penderitaan.
Untuk membungkus apa yang kita tahu mengenai situasi saat ini, kita akan
membuat sebuah kelas dengan nama WorldView
yang apabila kita mendesain
aplikasi ini di Akka, WorldView
bisa jadi merupakan sebuah var
pada
sebuah Actor
yang dapat berubah-ubah.
WorldView
menyatukan semua nilai kembalian dari semua metoda pada
aljabar-aljabar dan menambah sebuah bidang tertunda yang ditujukan
untuk menelusuri request mana saja yang belum terpenuhi.
Walaupun kita sudah siap menulis logika bisnis kita, kita harus menunjukkan
secara eksplisit bahwa kita bergantung pada Drone
dan Machines
.
Kita bisa menulis antarmuka untuk logika bisnis
dan mengimplementasikannya dengan sebuah modul.
Sebuah modul yang hanya bergantung ke modul-modul lain, aljabar dan fungsi
murni, dan dapat diabstraksikan melalui F
. Jika sebuah implementasi dari
sebuah antarmuka aljabaris terikat spesifik
pada sebuah tipe, misalkan IO
, implementasi tersebut disebut sebagai
sebuah penerjemah
Pembatasan konteks Monad
menunjukkan bahwa F
bersifat monad, memperkenankan
kita menggunakan map
, pure
, dan flatMap
melalui komprehensi for
.
Kita punya akses ke aljabar yang dimiliki oleh Drone
dan Machines
dengan
simbol D
dan M
. Penggunaan simbol satu huruf kapital merupakan ijtima
untuk implementasi monad dan aljabar.
Logika bisnis kita akan berjalan pada sebuah ikalan tak-hingga
3.3.1 initial
Pada initial
, kita memanggil semua layanan eksternal dan mengagregasi
semua hasilnya menjadi sebuah WorldView
.
Untuk nilai bawaan pending
, kita akan mengisinya dengan sebuah Map
kosong.
Sebagaimana yang sudah dibahas pada bab 1, flatMap
memperkenankan kita
melakukan operasi pada nilai yang dihasilkan pada waktu jalan.
Saat kita mengembalikan sebuah F[_]
, kita mengembalikan sebuah program
lain yang akan diterjemahkan saat waktu secara berurutan dan pada akhirnya,
kita bisa melakukan flatMap
padanya.
Beginilah cara kita menyambung beberapa kode yang berefek samping yang berurutan
secara aman. Saat itu pula, kita juga bisa menyediakan implementasi murni
untuk tes.
3.3.2 update
update
harus memanggil initial
untuk memperbarui worldview
kita
sembari mempertahankan tindakan tindakan yang masih pending
.
Ketika sebuah node mengalami perubahan keadaan, kita akan
menghapusnya dari pending
. Dan bila sebuah tindakan yang masih tertunda (pending)
masih belum mengerjakan apapun setelah menunggu 10 menit, maka kita
akan menganggapnya sebagai sebuah kegagalan.
Fungsi konkret semacam .symdiff
tidak memerlukan test dikarenakan mereka
mempunyai masukan dan keluaran yang eksplisit. Sehingga, kita dapat memindahkan
semua kode murni ke metoda metoda mandiri pada object
independen.
Testing metoda publik akan dengan senang hati kita lakukan.
3.3.3 act
Metoda act
sedikit lebih kompleks dibandingkan dengan metoda sebelumnya.
Untuk memperjelas maksud dan memudahkan pemahaman, kita akan membaginya
menjadi dua bagian. Pertama, mendeteksi kapankah sebuah aksi harus diambil.
Dan kedua, melakukan aksi yang sudah ditentukan.
Penyederhanaan ini juga berarti bahwa kita hanya bisa melakukan satu aksi
dalam sekali penyelawatan. Namun, hal tersebut cukup masuk
akal dikarenakan kita dapat mengontrol penyelawatan dan bisa juga menjalankan
ulang act
sampai tidak ada lagi yang perlu dilakukan.
Sebagai pengekstrak untuk WorldView
, kita akan menulis pendeteksi skenario
yang tidak lain dan tidak bukan hanyalah penulisan percabangan if
dan else
yang jauh lebih ekspresif dibandingkan yang biasa!
Adalah sebuah keharusan untuk menambah agen ke peternakan bila ada timbunan pekerjaan, atau saat kita tidak punya agen, atau ketika tak ada node yang menyala, juga tidak ada aksi aksi yang sedang ditunda Caranya? Tentu dengan mengembalikan kandidat node yang ingin kita jalankan:
Bila tidak ada timbunan pekerjaan, kita harus menghentikan semua node yang sudah basi (pengangguran / tidak punya pekerjaan). Akan tetapi, jangan lupa bahwa Google menarik bayaran berdasarkan waktu penggunaan (dalam hitungan jam), maka kita akan mematikan mesin tersebut pada menit ke 58 agar kita IRIT! Disini, kita akan mengembalikan daftar non-kosong dari node untuk dihentikan.
Agar IRIT, semua node harus mati sebelum 5 jam.
Setelah kita berhasil mendeteksi skenario-skenario yang mungkin terjadi,
kita bisa melanjutkan dengan menulis metoda act
.
Saat kita menjadwalkan sebuah node untuk dijalankan atau dibunuh, kita
bisa menambahkan node tersebut ke pending
sambil mencatat waktu yang
penjadwalan aksi tadi.
Karena NeedsAgent
dan Stale
tidak menutup semua kemungkinan yang bisa
terjadi, maka kita butuh jaring pengaman cace _
yang sebenarnya tidak
melakukan apapun.
Saudara bisa mengingat kembali bab 2 dimana .pure
menciptakan konteks monadik
dari sebah nilai (for
).
foldLeftM
sendiri sebenarnya mirip dengan foldLeft
. Namun, tiap iterasi
dari penekukan (“fold”) bisa saja menghasilkan sebuah nilai monadik.
Pada kasus ini, tiap iterasi dari tiap tekukan mengembalikan F[WorldView]
.
Simbol M
pada M.stop(n)
, misal, melambangkan bahwa ekspresi tersebut
bersifat monadik. Kita akan banyak menemukan banyak metoda “lifted”
seperti ini yang hanya mau menerima nilai nilai monadik, bukan nilai biasa.
3.4 Tes Unit
Pendekatan seperti ini, yang digunakan pada pemrograman fungsional, adalah hal yang diimpikan oleh seorang arsitek dimana detail implementasi atas aljabar-aljabar diserahkan kepada anggota tim dan sang arsitek fokus dalam menentukan logika bisnis untuk memenuhi tuntutan bisnis.
Aplikasi kita ini sangat bergantung pada tempo dan layanan web pihak ketiga. Bila kita sedang menulis aplikasi ini dengan metodologi OOP tradisionil, kita akan membuat tiruan untuk semua metoda yang digunakan untuk memanggil layanan tersebut ataupun aktor aktor untuk pesan pesan keluar. Peniruan yang digunakan pada pemrograman fungsional dilakukan dengan cara membuat implementasi alternatif dari aljabar yang digunakan. Tiap aljabar tiruan tadi, mengisolasi bagian bagian dari sistem yang harus ditiru.
Kita bisa memulainya dengan data data yang dikhususkan untuk testing.
Kita bisa mengimplementasikan aljabar-aljabar yang akan ditiru dengan
mengekstensi Drone
dan Machines
dengan konteks monadik spesifik,
seperti Id
sebagai contoh konteks yang paling sederhana.
Implementasi tiruan kita hanya memutar ulang sebuah WorldView
yang tetap.
Kita mengisolasi keadaan sistem kita sehingga kita dapat menggunakan
var
untuk menyimpan keadaan tersebut.
Ketika kita menulis sebuah unit tes, kita akan membuat sebuan instans Mutable
dan mengimpor semua anggotanya.
Baik drone
maupun machines
kita, menggunakan konteks eksekusi
Id
. Sehingga, program ini akan mengembalikan sebuah Id[WorldView]
yang bisa kita tegaskan.
Sebenarnya, pada kasus remeh seperti ini, kita tinggal memeriksa
apakah metoda initial
memang betul mengembalikan nilai yang sama
dengan yang kita gunakan dalam implementasi statik.
Kita juga bisa membuat tes yang lebih rumit untuk metoda update
dan act
untuk membantu kita menghilangkan kutu-kutu dan memperhalus
persyaratan.
Akan menjadi sebuah kebosanan yang nyata bila kita harus berbincang secara panjang dan lebar mengenai semua rangkaian tes. Tes-tes berikut sebenarnya bisa dengan mudah diimplementasikan dengan menggunakan pendekatan yang sama.
- jangan meminta agen saat pending.
- jangan mematikan agen bila node masih muda.
- matikan agen bila tidak ada timbunan pekerjaan dan node akan makan biaya lagi.
- jangan matikan agen bila masih ada tindakan yang pending.
- matikan agen bila tidak ada backlog jika terlalu tua.
- matikan agen bila sudah tua, termasuk yang sedang mengerjakan sesuatu.
- abaikan tindakan-tindakan yang tidak responsif saat pemutakhiran.
Semua tes di atas dijalankan secara berurutan dan terisolasi terhadap ulir penjalan test (yang bisa jadi dijalankan secara paralel). Bilamana kita mendesain rangkaian tes kita di Akka, tes-tes kita bisa jadi menjadi korban atas kesewenangan habisnya waktu. Belum lagi dengan disembunyikannya galat-galat pada berkas log.
Bukan melebih-lebihkan, namun kenyataan bahwa testing untuk logika bisnis pada aplikasi kita memang meningkat drastis. Anggap saja bahwa 90% waktu yang digunakan oleh pengembang aplikasi bersama dengan pelanggan dihabiskan untuk memperhalus, memperbarui, dan memperbaiki aturan aturan bisnis ini, tentu yang lainnya merupakan detail saja.
3.5 Paralel
Saat ini, aplikasi yang sudah kita desain menjalankan metoda-metoda aljabar secara berurutan. Namun, ada beberapa bagian-bagian yang bisa dijalankan secara paralel.
3.5.1 initial
Pada definisi kita atas initial
, kita dapat meminta semua informasi
yang kita butuhkan pada saat yang sama. Sehingga, kita tidak perlu
melakukan satu kueri dalam satu waktu.
Berbeda halnya dengan flatMap
untuk operasi berurutan, Scalaz menggunakan
sintaksis Apply
untuk operasi paralel:
yang bisa juga dituliskan menggunakan notasi infiks:
Bila setiap operasi paralel mengembalikan sebuah nilai pada konteks
monadik yang sama, kita dapat menerapkan sebuah fungsi ke hasil-hasilya
saat mereka kembali.
Metoda initial
bisa ditulis ulang sebagai berikut.
3.5.2 act
Pada logika yang saat ini digunakan untuk act
, kita menghentikan
setiap node secara berurutan sembari menunggu hasil proses penghentian
tersebut, baru melanjutkan penghentian node lainnya.
Padahal, kita dapat menghentikan semua node bersamaan dan dilanjutkan
dengan memutakhirkan worldview
kita.
Salah satu kekurangan dari cara ini adalah ketika sebuah operasi gagal
dilakukan, maka proses akan berhenti lebih awal sebelum kita memutakhirkan
bidang pending
.
Sebenarnya kompromi semacam ini masih masuk akal karena metoda update
kita akan dengan anggun menangani kondisi dimana sebuah
node
mati mendadak.
Untuk tipe data NonEmptyList
, kita butuh sebuah metoda yang mampu
melakukan pemetaan (map
ping) atas semua elemennya ke sebuah
F[MachineNode]
dan menghasilkan sebuah F[NonEmptyList[MachineNode]]
.
Cara ini disebut sebagai traverse
(pelintang) dan ketika kita melakukan
flatMap
, kita akan mendapatkan sebuah NonEmptyList[MachineNode]
yang bisa kita tangani dengan cara yang sederhana.
Saya kira, cuplikan diatas lebih mudah dipahami bila dibandingkan dengan versi yang berurutan.
3.6 Kesimpulan
- aljabar mendefinisikan antarmuka antar sistem.
- modul merupakan implementasi dari sebuah aljabar dalam bentuk aljabar lain.
-
interpreter merupakan implementasi konkret dari sebuah aljabar untuk sebuah
F[_]
tetap. - Interpreter tes dapat mengganti bagian bagian yang mempunyai efek samping pada sistem dan memberikan cakupan tes yang lebih tinggi.
4. Data dan Fungsionalitas
Pada OOP, kita biasa berpikir mengenai data dan fungsionalitas dalam satu bentuk, kelas. Hierarki kelas berisi metoda-metoda dan trait yang memaksa bidang bidang data ada pada kelas yang memakainya. Polimorfisme dari sebuah objek saat waktu-jalan dilihat dengan kacamata hubungan “merupakan”, yang mengkehendaki kelas kelas untuk mewariskan dari antarmuka-antarmuka umum. Hal semacam ini bisa dapat memperkeruh bila basis kode menjadi besar. Tipe data tipe data sederhana bisa jadi kabur batasannya karena ratusan baris metoda, trait menjadi kacau karena urutan inisiasi yang salah, dan testing maupun peniruan komponen yang melekat kuat menjadi kelagepan.
PF mengambil pendekatan yang berbeda dengan mendefinisikan data dan fungsionalitas secara terpisah. Pada bab ini, kita akan membahas tipe data tipe data dasar dan keuntungan dari pembatasan diri kita kepada subset Scala. Kita juga akan menemukan tipe kelas sebagai sebuah cara untuk mencapai polimorfisme waktu-jalan dengan melihat fungsionalitas dari sebuah struktur data dengan kacamata hubungan “memiliki” bukan hubungan “merupakan”.
4.1 Data
Blok bangunan mendasar dari tipe data adalah
-
final case class
yang juga dikenal sebagai produk -
sealed abstract class
yang juga dikenal sebagai ko-produk -
case object
danInt
,Double
,String
(dll) sebagai nilai
tanpa metoda ataupun bidang selain parameter konstruktor.
Kita lebih memilih abstract class
dibandingkan trait
dengan alasan
agar mendapatkan kompatibilitas biner dan juga me-makruh-kan pencampuran “mixin”
Ketiga tipe data diatas, secara paket, disebut juga dengan Tipe Data Aljabar (TDA).
Sebagai contoh, kita menyusun tipe data dari aljabar Boolean AND
dan XOR
:
sebuah produk berisi semua tipe yang terdiri darinya, namun sebuah ko-produk
hanya dapat menjadi satu-satunya. Sebagai contoh
- produk:
ABC = a AND b AND c
- ko-produk:
XYZ = x XOR y XOR z
yang bila ditulis menggunakan Scala
4.1.1 TDA Tergeneralisasi
Ketika kita mengenalkan sebuah parameter tipe ke sebuah ADT, kita menyebutnya dengan Tipe Data Aljabar Tergenerealisasi (Generalised Algebraic Data Type).
scalaz.IList
, yang merupakan alternatif yang lebih aman dari pustaka
standar List
, adalah sebuah TDAT:
Bila sebuah TDA mengacu pada dirinya sendiri, kita menyebutnya sebagai
sebuah tipe rekursif. IList
sendiri merupakan contoh tipe rekursif
karena ICons
berisi referensi ke sebuah IList
.
4.1.2 Fungsi pada TDA
ADTs can contain pure functions
TDA bisa berisi fungsi murni
Namun TDA yang berisi fungsi mempunyai beberapa kekurangan karena
ADT tersebut tidak bisa diterjemahkan dengan sempurna ke JVM.
Sebagai contoh, warisan Serializable
, hashCode
, equals
, dan toString
tidak berperilaku sebagaimana yang diharapkan.
Dan yang menjadi sebuah kekecewaan adalah, Serializable
sangat jamak
digunakan oleh framework populer walaupun alternatif yang lebih
bagus banyak.
Salah satu jebakan yang umum memakan korban adalah seorang penulis
lupa bahwa Serializable
bisa jadi berusaha untuk menyerikan fungsi
closure secara keseluruhan dan bisa berakibat server rhemuk!
Kekurangan lain yang mirip juga sama terjadi pada kelas Java peninggalan
jaman dulu seperti Throwable
, yang bisa saja berisi rujukan pada objek
arbiter.
Kita akan mengeksplorasi alternatif alternatif untuk metoda peninggalan sejarah saat kita berbincang mengenai pustaka Scalaz pada bab berikutnya. Tentu dengan mengorbankan interoperabilitas dengan kode kode Scala dan Java peninggalan sejarah.
4.1.3 Kelengkapan
Adalah hal yang penting untuk kita menggunakan sealed abstract class
,
dan tidak hanya abstract class
, saat kita mendefinisikan sebuah tipe
data.
Dengan menyegel (seal
) sebuah class
, kita juga memastikan bahwa
semua sub-tipe-nya harus didefinisikan di berkas yang sama.
Hal ini memberikan kesempatan agar kompilator bisa mengetahui hubungan
mereka, sehingga kompilator bisa memeriksa keluwesan “pattern match”
dan juga pada makro yang menghilangkan plat cetak. Sebagai contoh,
Cuplikan diatas menunjukkan kepada pengembang apa yang telah mereka
rusak ketika menambah sebuah produk baru ke basis kode.
Hal ini terjadi karena kita menggunakan ekstensi -Xfatal-warnings
sehingga semua peringatan dari kompilator menjadi galat.
Namun, kompilator juga tidak akan memeriksa kelengkapan bila kelas tidak tersegel ataupun ada pengaman lain. Misal
Agar tetap aman, jangan gunakan pengaman ketika menggunakan tipe tersegel.
Panji -Xstrict-patmat-analysis
sudah diajukan sebagai peningkatan bahasa untuk menampah pemeriksaan
“pattern match” tambahan
4.1.4 Produk dan Koproduk Alternatif
Bentuk lain dari produk adalah tuple yang merupakan sebuah final case class
tanpa label.
(A.type, B, C)
ekuivalen denga ABC
pada contoh di atas.
Namun, sangat disarankan untuk menggunakan final case class
ketika
digunakan pada sebuah ADT. Selain karena agak canggung bila tanpa nama,
case class
juga mempunyai performa yang jauh lebih baik bila dibandingkan
dengan nilai-nilai primitif.
Contoh lain dari ko-produk adalah saat kita melapisi tipe Either
.
equivalent to the XYZ
sealed abstract class. A cleaner syntax to define
nested Either
types is to create an alias type ending with a colon,
allowing infix notation with association from the right:
yang ekuivalen dengan kelas abstrak tersegel XYZ
.
Untuk sintaks yang lebih rapi yang digunakan untuk mendefinisikan
tipe Either
berlapis, pengguna dapat menggunakan alias tipe yang diakhiri
dengan titik dua. Hal ini memperkenankan penggunaan notasi infiks dengan
asosiasi sebelah kanan:
Cara ini berguna untuk membuat ko-produk anonim saat kita tidak dapat meletakkan semua implementasi dalam sebuah berkas kode yang sama.
Alternatif lain dari ko-produk adalah dengan membuat sealed abstract class
khusus dengan definisi final case class
yang hanya membungkus tipe
yang diinginkan.
Pencocokan pola pada bentuk bentuk ko-produk ini bisa jadi sangat boyak. Hal ini juga yang melatar belakangi eksplorasi Tipe Gabungan pada kompilator baru Scala, Dotty. Makro seperti totalitarian dan iotaz hadir sebagai alternatif untuk menyandikan ko-produk anonim.
4.1.5 Penyampaian Informasi
Selain digunakan sebagai kontainer untuk informasi bisnis, tipe data juga bisa digunakan untuk menyandikan batasan. Sebagai contoh,
tidak bisa kosong. Hal inilah yang menjadikan scalaz.NonEmptyList
sebuah tipe data yang penting walaupun mempunyai informasi yang sama
dengan IList
.
Tipe produk sering kali berisi tipe yang jauh lebih umum daripada yang diharapkan. Pada OOP tradisional, hal ini diatasi dengan menggunakan validasi input dan penegasan.
Sebagai gantinya, kita dapat menggunakan tipe data Either
untuk menyediakan
Right[Person]
untuk instans valid. Tidak hanya itu, penggunaan Either
juga
bisa mengurangi kemungkinan instansiasi yang seharusnya tidak mungkin terjadi.
Pada contoh selanjutnya, harap diperhatikan bahwa konstruktor kelas Person
dibuat sebagai final.
4.1.5.1 Tipe Data Terrefinasi
Selain dengan menggunakan Either
sebagaimana yang telah dicontohkan pada
bagian sebelumnya, ada juga cara yang lebih mudah dan rapi yaitu dengan
menggunakan pustaka refined
.
Pustaka tersebut memberikan batasan batasan untuk tipe data yang bisa
digunakan pada sebuah kelas.
Untuk memasang pustaka tersebut, silakan tambahkan baris berikut pada
build.sbt
.
dan baris-baris berikut pada kode sumber.
Refined
memberikan batasan batasan yang jauh lebih jelas dengan menuliskan
A Refined B
.
Nilai pokok bisa didapatkan dengan memanggil metoda .value
. Sedangkan
untuk membuat nilai refined
pada saat waktu jalan, kita bisa menggunakan
.refineV
yang mengembalikan Either
.
Bila kita menambah impor berikut,
kita dapat menyusun nilai nilai valid saat waktu kompile dan akan mendapatkan pesan galat ketika nilai yang disediakan tidak memenuhi kriteria yang diminta.
Untuk kriteria yang lebih kompleks, kita dapat menggunakan aturan MaxSize
pada
contoh berikut.
Untuk memenuhi persyaratan bahwa String
harus tidak kosong dan mempunyai
panjang maksimal 10 karakter, kita bisa menulis sebagai berikut:
Bila kita menemui persyaratan-persyaratan yang tidak didukung oleh pustaka
refined
, kita dapat dengan mudah menyusunnya sendiri.
Sebagai contoh, pada drone-dynamic-agents
, kita harus memastikan bahwa
sebuah String
harus mengandung application/x-www-form-urlencoded
.
Untuk menyusunnya, kita bisa menggunakan pustaka standar regex Java.
4.1.6 Sederhana untuk Dibagi
Dengan tidak berisi fungsionalitas apapun, sangat mungkin sebuah ADT memiliki ketergantungan yang kecil. Hal ini-lah yang memudahkan kita untuk berbagi dengan pengembang lain. Dengan menggunakan bahasa pemodelan data sederhana, interaksi antar tim inter-disipliner akan lebih mudah dan ketergantungan atas dokumen tertulis berkurang.
Terlebih lagi, peralatan yang digunakan bisa dibuat dengan mudah agar dapat menghasilkan atau menggunakan skema dari bahasa pemrograman lain dan protokol komunikasi.
4.1.7 Menghitung Kompleksitas
Kompleksitas dari sebuah tipe data diambil dari jumlah nilai yang bisa ada pada tipe data tersebut. Sebuah tipe data yang bagus mempunyai tingkat kompleksitas yang rendah bila dibandingkan dengan informasi yang disampaikan.
Nilai-nilai berikut punya kompleksitas yang tetap.
-
Unit
punya satu nilai. -
Boolean
punya dua nilai. -
Int
punya 4,294,967,295 nilai. -
String
bisa dibilang punya nilai tak hingga.
Untuk mencari kompleksitas dari sebuah produk, kita tinggal mengalikan kompleksitas dari tiap bagian.
-
(Boolean, Boolean)
punya 4 nilai (2*2
) -
(Boolean, Boolean, Boolean)
punya 8 nilai (2*2*2
)
Sedangkan untuk mencari kompleksitas dari sebuah ko-produk, kita tinggal menambah kompleksitas dari tiap bagian.
-
(Boolean |: Boolean)
punya 4 nilai (2+2
) -
(Boolean |: Boolean |: Boolean)
punya 6 nilai (2+2+2
)
Sedangkan untuk mencari kompleksitas dari sebuah GADT, kalikan tiap bagian dengan kompleksitas dari setiap parameter.
-
Option[Boolean]
punya 3 nilai,Some[Boolean]
danNone
(2+1
)
Pada pemrograman fungsional, selain fungsi harus total, juga harus mempunyai nilai kembalian untuk semua input, tak pengecualian.. Praktik utama yang digunakan untuk mencapai totalitas adalah dengan mengurangi jumlah input dan output. Sebagai patokan, tanda tanda fungsi yang tidak didesain dengan seksama adalah ketika kompleksitas dari output sebuah fungsi lebih besar daripada jumlah perkalian inputnya.
Kompleksitas dari sebuah fungsi total adalah jumlah fungsi yang bisa memenuhi signature dari fungsi tersebut yang dihitung dengan menggunakan output pangkat input.
-
Unit => Boolean
punya kompleksitas 2. -
Boolean => Boolean
punya kompleksitas 4. -
Option[Boolean] => Option[Boolean]
punya kompleksitas 27. -
Boolean => Int
dari quintillion jadi sextillion. -
Int => Boolean
sedemikian besar bila semua implementasi ditetapkan pada sebuah angka unik, tiap implementasi membutuhkan ruang 4 gigabita agar dapat direpresentasikan.
Kenyataannya, Int => Boolean
bisa jadi hanya sebuah fungsi sederhana seperti
isOdd
, isEven
, atau BitSet
. Fungsi ini, ketika digunakan pada sebuah ADT,
bisa diganti dengan menggunakan ko-produk untuk menandai fungsi yang relevan.
Ketika kompleksitas fungsi kita adalah “semua boleh masuk dan semua bisa keluar”, kita harus memberikan tipe data yang terbatas dan proses validasi. etc
Keuntungan lain yang bisa didapat saat kita bisa menghitung kompleksitas penanda tipe adalah kita bisa mencari penanda tipe yang lebih sederhana dengan aljabar tingkat SMP. Untuk menghitung kompleksitasnya, tinggal mengganti
-
Either[A, B]
dengana + b
-
(A, B)
dengana * b
-
A => B
denganb ^ a
dilanjutkan dengan mengurutkan, lalu tinggal konversi balik. Sebagai contoh, misalkan kita mendesain sebuah kerangka kerja berdasarkan callbacks dan pada akhirnya kita membuat penanda tipe sebagai berikut:
Yang bisa kita konversi dan atur ulang sebagai
dan pada akhirnya, kita bisa konversi ulang ke tipe dan mendapat:
yang jauh lebih sederhana. Kita cuma perlu untuk menyuruh pengguna untuk
menyediakan Either[A, B] => C
.
Dengan penalaran yang sama, kita bisa membuktikan bahwa
ekuivalen dengan
yang dikenal dengan Currying
4.1.8 Pilih Koproduk, bukan Produk
Sebuah masalah pemodelan dasar yang sering kali muncul adalah ketika
ada beberapa parameter konfigurasi yang saling ekslusif yang sebut saja
a
, b
, dan c
.
Produk (a: Boolean, b: Boolean, c: Boolean)
punya kompleksitas 8
sedangkan ko-produk
punya kompleksitas 3. Sebagaimana yang telah ditunjukkan di atas, adalah lebih disukai untuk memodelkan parameter konfigurasi ini sebagai ko-produk bila dibandingkan dengan memberikan kemungkinan 5 kondisi invalid terjadi.
Kompleksitas dari sebuah tipe data juga mempunyai implikasi pada testing. Di lapangan, adalah hal yang mustahil untuk memeriksa semua input yang mungkin terjadi untuk sebuah fungsi. Sebaliknya, dengan mengecek sedikit sampel dari sebuah tipe data dengan Scalacheck jauh lebih mudah. Bila sebuah sampel dari sebuah tipe data punya probabilitas valid rendah, hal tersebut merupakan pertanda bahwa pemodelan data dilakukan secara kurang tepat.
4.1.9 Pengoptimalan
Keuntungan yang sangat terasa saat menggunakan subset sederhana Scala untuk merepresentasikan tipe data adalah tooling dapat melakukan optimisasi atas representasi bytecode JVM.
Sebagai contoh, kita dapat mengemas bidang Boolean
dan Option
ke dalam
sebuah Array[Byte]
, menyimpan nilai di tembolok, memoisasi hashCode
,
optimisasi equals
, menggunakan statemen @switch
pada saat pattern match,
dan banyak lagi.
Pengoptimalan semacam ini tidak bisa diterapkan pada hierarki class
di
OOP yang juga mengatur “state”, melempar eksepsi, ataupun menyediakan
implementasi metoda adhoc.
4.2 Fungsionalitas
Fungsi murni biasanya didefinisikan sebagai metoda pada sebuah objek.
Sebagaimana yang kita lihat pada cuplikan di atas, penggunaan metoda
pada object bisa jadi terlihat kikuk. Selain karena terbaca dari dalam
ke luar (bukan kiri ke kanan), juga karena objek tersebut menggunakan
“namespace”.
Bila kita mendefinisikan sin(t: T)
di tempat lain, kita akan mendapat
galat referensi ambigu. Bila pembaca pernah mengalami masalah saat
menggunakan metoda statik dan metoda kelas pada Java, hal yang sama
juga terjadi bila menggunakan metoda objek pada Scala.
Dengan menggunakan fitur implicit class
dan sedikit plat cetak, kita dapat
menggunakan gaya penulisan yang familiar:
Sering kali, lebih disukai bila kita melewatkan pendefinisian object
dan langsung mendefinisikan implicit class
untuk mengurangi plat cetak:
4.2.1 Fungsi Polimorfis
Jenis fungsi yang lebih umum adalah fungsi polimorfis yang biasa ada pada sebuah kelas tipe. Sebuah tipe kelas merupakan ciri yang:
- tidak berisi keadaan.
- mempunyai parameter tipe.
- mempunyai, setidaknya, satu metoda abstrak (kombinator primitif).
- mungkin berisi metoda yang terumumkan (kombinator turunan).
- mungkin berupa perpanjangan dari kelas tipe lain.
Untuk semua tipe parameter, hanya boleh ada satu implementasi kelas tipe. Properti ini dikenal sebagai koherensi kelas tipe. Kelas tipe secara sekilas, terlihat seperti antarmuka aljabaris di bab sebelumnya. Namun, aljabar tidak harus koheren.
Pustaka standar Scala juga berisi kelas tipe. Kita akan mengeksplorasi
scala.math.Numeric
yang disederhanakan untuk menunjukkan prinsip prinsip
dari kelas tipe:
Kita dapat melihat semua fitur utama dari sebuah kelas tipe pada cuplikan kode di atas:
- Tidak ada keadaan
-
Ordering
danNumeric
mempunyai parameter tipeT
. -
Ordering
mempunyai metoda abstrakcompare
danNumeric
mempunya metoda abstrakplus
,times
,negate
, danzero
. -
Ordering
mendefinisikan metodalt
dangt
yang sudah digeneralisasi yang didasarkan padacompare
.Numeric
mendefinisikanabs
dengan menggunakanlt
,negate
, danzero
. -
Numeric
merupakan perpanjangan dariOrdering
.
Sekarang kita dapat membuat fungsi untuk tipe yang memiliki kelas tipe Numeric
:
Kita tidak lagi bergantung kepada hierarki OOP untuk tipe input kita.
Dengan kata lain, kita tidak meminta input kita “merupakan sebuah”
Numeric
. Hal ini sangat penting bila kita ingin mendukung kelas dari
pihak ketiga yang tidak mungkin kita definisikan ulang.
Keuntungan lain dari kelas tipe adalah pengasosiasian fungsionalitas ke data dilakukan saat kompilasi. Hal yang berbeda terjadi pada OOP dimana dilakukan “dynamic dispatch” pada wakut jalan.
Sebagai contoh, dimana kelas List
hanya bisa mempunya satu implementasi
sebuah metoda, sebuah metoda kelas tipe bisa memberikan kita beberapa
implementasi yang begantung pada konten List
.
Sehingga, terjadi pemindahan beban kerja dari waktu jalan ke waktu kompilasi.
4.2.2 Sintaks
Ada beberapa hal yang bisa dirapikan pada sintaks signOfTheTimes
yang terlihat kikuk.
Pengguna hilir akan lebih senang bila mereka dapat melihat metoda kita
menggunakan konteks terikat karena penanda dapat terbaca
dengan jelas bahwa metoda tersebut menerima parameter T
yang,
misal, merupakan Numeric
walaupun hal itu berarti kita harus selalu menggunakan implicitly[Numeric[T]]
.
Dengan mendefinisikan plat cetak pada kelas tipe,
kita bisa mengurangi derau untuk implicit
.
Namun hal semacam ini tetap saja buruk bagi kita sebagai pengimplementasi.
Kita mempunyai masalah sintaksis dari metoda statik dalam-ke-luar atau
metoda kelas. Kita menangani hal ini dengan memperkenalkan ops
pada
objek pendamping kelas tipe:
Harap diperhatikan bahwa -x
akan dijabarkan menjadi x.unary_-
oleh
pemanis sintaksis kompilator. Oleh karena itu, kita mendefinisikan unary_-
sebagai sebuah metode perpanjangan. Sekarang, kita dapat menulis signOfTheTimes
dengan lebih rapi:
Langkah langkah diatas mungkin tidak perlu dilakukan bila menggunakan Simulacrum
yang menyediakan anotasi makro @typeclass
yang secara otomatis menghasilkan
apply
dan ops
. Pustaka ini juga menyediakan cara agar kita dapat mendefinisikan
nama alternatif (yang biasanya berupa simbol) untuk metoda-metoda umum.
Untuk lebih lengkapnya, bisa dilihat potongan kode berikut:
Saat ada simbol buatan @op
, simbol ini diucapkan seperti nama metoda-nya.
Misalkan simbol <
disebut sebagai “kurang dari”, bukan “kurung”
4.2.3 Instans
Instans dari Numeric
(yang juga merupakan instans dari Ordering
)
didefinisikan sebagai sebuah implicit val
(nilai implicit) yang merupakan
perpanjangan dari kelas tipe dan dapat menyediakan implementasi teroptimisasi
dari metoda tergeneralisasi:
Walaupun kita menggunakan operator +
, *
, unary_-
, <
, dan >
,
metoda-metoda tersebut sebenarnya sudah ada pada Double
. Metoda kelas
biasanya lebih disukai daripada metoda perpanjangan. Dan faktanya,
kompilator Scala melakukan penanganan khusus untuk primitif dan mengubah
metoda ini menjadi instruksi bytecode asli seperti dadd
, dmul
, dcmpl
,
dan dcmpg
.
Kita juga bisa mengimplementasikan Numeric
untuk kelas BigDecimal
milik
Java (bukan scala.BigDecimal
yang rhemuk)
Kita bisa membuat struktur data kita sendiri untuk bilangan kompleks:
Dan menurunkan Numeric[Complex[T]]
bila Numeric[T]
sudah ada.
Karena instans ini bergantung pada parameter tipe, penurunan ini menggunakan
def
, bukan val
.
Pembaca yang jeli mungkin memperhatikan bahwa abs
tidak sesuai
dengan apa yang matematikawan harapkan. Nilai kembalian untuk abs
seharusnya berupa T
, bukan Complex[T]
.
scala.math.Numeric
mencoba untuk melakukan terlalu banyak hal dan
tidak tergeneralisasi diluar bilangan nyata. Hal ini bisa jadi pelajaran
yang bagus bahwa kelas tipe yang kecil dan terdefinisi dengan baik sering
kali lebih baik daripada koleksi monolitik yang terdiri dari fitur
fitur yang terlalu spesifik.
4.2.4 Resolusi Implisit
Kita sudah mendiskusikan mengenai implisit secara panjang lebar. Bagian ini akan berbicara mengenai apakah implisit itu dan bagaimana cara mereka bekerja.
Parameter implisit adalah saat sebuah metoda meminta instan khusus dari sebuah tipe tertentu yang ada pada cakupan implisit dari pemanggil dengan sintaks khusus untuk instans kelas tipe. Parameter implisit merupakan cara yang lebih rapi dalam menggalur konfigurasi pada sebuah aplikasi.
Pada contoh ini, foo
meminta instans dari Numeric
dan Typeable
yang
tersedia untuk A
dan juga sebuah objek Handler
implisit yang meminta
dua parameter.
Konversi implisit adalah ketika sebuah implicit def
ada. Salah
satu penggunaan konversi implisit adalah untuk pembuatan perpanjangan
metodologi. Ketika kompilator menyelesaikan pemanggilan sebuah metoda,
kompilator pertama tama akan memeriksa apakah metoda tersebut ada pada
tipe, yang dilanjutkan kepada bapaknya (aturan yang mirip dengan Java).
Bila gagal menemukan yang cocok, kompilator akan mencari cakupan implisit
untuk konversi ke tipe lain. Baru dilanjutkan dengan pencarian
untuk tipe-tipe tersebut.
Penggunaan lain untuk konversi implisit adalah dengan derivasi kelas tipe.
Pada bagian sebelumnya, kita menulis sebuah implicit def
yang diturunkan dari Numeric[Complex[T]]
bila sebuah Numeric[T]
ada pada cakupan implisit. Adalah sebuah hal
yang mungkin untuk merangkai banyak implicit def
(juga secara rekursif).
Hal ini juga merupakan basis dari pemrograman dengan tipe yang
memindahkan komputasi untuk dilakukan pada saat kompilasi daripada
saat waktu jalan.
Perekat yang menggabungkan parameter implisit dengan konversi implisit adalah resolusi implisit.
Pertama, cakupan variabel normal dicari dengan urutan:
- cakupan lokal, termasuk impor tercakup. (mis, blok atau metoda)
- cakupan luar, termasuk impor tercakup. (mis, anggota pada kelas)
- kelas orangtua
- objek dari paket saat ini.
- objek dari kelas orang tua.
- impor pada berkas.
Bila semua gagal mencari yang cocok, maka pencarian pada cakupan khusus akan dilakukan. Pencarian ini dikhususkan untuk instans implisit yang ada pada objek pasangan, objek paket, objek luar (bila berlapis), dan diulang untuk kelas induk. Pencarian ini dilakukan dengan urutan sebagai berikut:
- tipe parameter yang ada.
- tipe parameter yang diminta.
- parameter tipe (bila ada).
Bila ada dua implisit yang sesuai diketemukan pada resolusi implisit yang sama, galat implisit ambigu akan dilempar.
Implisit seringkali didefinisikan pada sebuah trait
, yang biasanya
akan diperpanjang oleh sebuah objek. Hal ini dilakukan untuk mengotrol
prioritas dari sebuah implisit, relatif terhadap implisit lain yang lebih
spesifik, untuk mencegah implisit yang ambigu.
Spesifikasi Bahasa Scala cenderung kabur untuk kasus kasus yang kurang umum
dan implementasi kompilator-lah yang menjadi standar de-fakto. Ada beberapa
patokan yang akan kita gunakan sepanjang buku ini. Misalkan, kita akan
memilih implicit val
dibandingkan implicit object
meskipun akan ada
godaan untuk menulis lebih pendek. ini adalah perilaku unik atas resolusi implisit
yang memperlakukan imlicit object
tidak sama saat memperlakukan implicit val
.
Resolusi implisit gagal saat ada hierarki kelas tipe seperti Ordering
dan Numeric
.
Bila kita menulis fungsi yang mengambil sebuah Ordering
implicit, dan kita memanggilnya
untuk sebuah tipe primitif yang punya instans Numeric
yang terdefinisi pada pasangan Numeric
,
kompilator akan gagal mencarinya.
Resolusi implisit seringkali untung-untungan bila kelas tipe digunakan
saat bentuk dari parameter imlisit berubah. Sebagai contoh, sebuah parameter
implisit menggunakan sebuah alias seperti type Values[A] = List[Option[A]]
mungkin
akan gagal untuk mencari implisit yang definisikan sebagai List[Option[A]]
.
Hal ini disebabkan karena bentuknya berubah dari thing of things dari A
menjadi thing dari A
.
4.3 Memodelkan OAuth2
Kita akan menutup bab ini dengan contoh praktikal dari pemodelan data dan derivasi kelas tipe dan aljabar / desain modul dari bab sebelumnya.
Pada aplikasi drone-dynamic-agents
kita, untuk berkomunikasi dengan Drone
dan Google Cloud, kita harus menggunakan JSON dengan REST. Kedua layanan tersebut
menggunakan OAuth2 untuk otentikasi.
Ada banyak dalam interpretasi OAuth2, namun kita akan fokus pada versi yang
cocok untuk Google Cloud. Bahkan, versi untuk Drone jauh lebih sederhana.
4.3.1 Deskripsi
Setiap aplikasi Google Cloud mengharuskan kita untuk mengatur OAuth 2.0 Client Key pada
Mendapatkan Client ID dan Client secret.
Lalu, aplikasi bisa mendapatkan satu kode setelah pengguna melakukan Permintaan Otorisasi pada peramban mereka. Kita harus membuka laman berikut pada peramban:
Kode yang dikirimkan ke {CALLBACK_URI}
dalam sebuah permintaan GET
.
Untuk menangkap informasi ini di aplikasi kita, kita harus mempunya sebuah
pelayan web yang mendengar ke localhost
.
Setelah kita punya kode tersebut, kita dapat melakukan Access Token Request:
yang akan memberikan jawaban berupa JSON.
Bearer token biasanya kadaluarsa setelah satu jam dan dapat disegarkan dengan mengirimkan sebuah permintaan HTTP dengan refresh token yang valid.
yang akan direspon dengan
Semua permintaan dari pengguna ke server harus mengikutsertakan tajuk
setelah mengganti dengan BEARER_TOKEN
yang sebenarnya.
Google hanya akan menerima 50 bearer token terakhir. Jadi, waktu kadaluarsa hanya merupakan panduan saja. Refresh token bertahan antar sesi dan dapat dibuat kadaluarsa secara manual oleh pengguna. Sehingga, kita memiliki aplikasi yang harus diatur sekali untuk mendapatkan “refresh token” dan mengikutsertakan “refresh token” sebagai konfigurasi untuk pemasangan server “headless”.
Drone tidak perlu mengimplementasikan “endpoint” /auth
atau refresh
karena sebuah BEARER_TOKEN
sudah cukup untuk antarmuka.
4.3.2 Data
Langkah pertama adalah memodelkan data yang dibutuhkan untuk OAuth2. Kita membuat
sebuah ADT dengan bidang yang sama persis dengan yang dibutuhkan oleh server OAuth2.
Kita akan menggunakan String
dan Long
dengan alasan keringkasan. Namun,
kita juga bisa menggunakan tipe “refined” bila bidang yang menggunakan String
dan
Long
tembus ke model bisnis kita.
4.3.3 Fungsionalitas
Kita juga harus menyusun kelas data yang telah kita definisikan pada bagian sebelumnya ke JSON, URL, dan borang yang dikodekan dalam POST. Kebutuhan seperti ini sangat bisa dipenuhi dengan menggunakan kelas tipe.
jsonformat
adalah pustaka JSON sederhana yang akan kita pelajari lebih seksama di bab yang akan datang.
Selain karena pustaka ini ditulis dengan pemrograman fungsional, juga karena didesain
dengan sedemikian rupa agar mudah dibaca. Pustaka ini terdiri dari sebuah AST JSON dan
kelas tipe penyandi dan pembaca sandi:
Kita butuh instans JsDecoder[AccessResponse]
dan JsDecoder[RefreshResponse]
dan kita dapat membuatnya dengan menggunakan fungsi bantuan:
Kita meletakkan instans tersebut pada pasangan dari tipe data kita. Sehingga, mereka akan selalu ada pada cakupan implisit:
Lalu, kita dapat menguraikan sebuah string ke AccessResponse
atau RefreshResponse
Kita dapat menulis tipe kelas kita sendiri untuk URL dan pengkodean POST. Berikut adalah desain yang masuk akal:
Kita harus menyediakan instans kelas tipe untuk tipe dasar:
Disini, kita menggunakan Refined.unsafeApply
ketika kita dapat menebak isi
dari string sudah berupa url terkode.
ilist
merupakan sebuah contoh dari penurunan sederhana dari kelas tipe,
yang kurang lebih satu tingkat dengan penurunan Numeric[Complex]
dari
representasi numerik. Metoda .intercalate
kurang lebih sama dengan .mkString
namun lebih umum.
Pada bab khusus pada Penurunan Kelas Tipe, kita akan mengkalkulasi instans dari
UrlQueryWriter
secara otomatis. Selain itu, kita akan merapikan apa yang
telah kita tulis. Untuk saat ini, kita akan menulis plat cetak untuk
tipe yang akan kita konversi:
4.3.4 Modul
Bagian sebelumnya melengkapi semua pemodelan data dan fungsionalitas yang dibutuhkan untuk mengimplementasikan OAuth2. Sebagaimana yang sudah dibahas pada bab sebelumnya, kita mendefinisikan komponen yang akan berinteraksi dengan dunia luar sebagai aljabar. Selain itu, kita akan mendefinisikan logika bisnis pada sebuah modul sehingga bisa dites dengan seksama.
Kita akan mendefinisikan ketergantungan aljabar dan menggunakan batasan konteks
untuk agar respon kita mempunyai JsDecoder
dan muatan POST
kita mempunyai
UrlEncodedWriter
:
Harap dicatat bahwa kita hanya mendefinisikan alur dengan asumsi terbaik
pada APA JsonClient
.
Untuk kejadian kejadian yang tidak diinginkan dan penangannya, kita akan
membicarakannya pada bab selanjutnya.
Untuk mendapatkan CodeToken
dari peladen OAuth2
Google, ada beberapa langkah
yang harus dilakukan.
- Memulai sebuah peladen HTTP pada mesin lokal dan mendapatkan nomor portnya.
- Memaksa pengguna untuk membuka sebuah laman web pada peramban mereka yang dimaksudkan agar mereka dapat masuk dengan menggunakan kredensial mereka mengizinkan aplikasi untuk menggunakan akun mereka, dan dilanjutkan dengan sebuah pengalihan balik ke mesin lokal.
- Mengambil kode token, menginformasikan kepada pengguna tentang langkah selanjutnya, lalu menutup peladen HTTP.
Kita dapat memodelkan langkah berikut dengan tiga metoda pada aljabar di UserInteraction
.
Mungkin pembaca budiman tidak percaya dengan cuplikan diatas. Akan tetapi, memang kenyataannya semudah itu.
Lalu, kita akan melanjutkan dengan abstraksi atas waktu pada sistem lokal.
Dan membuat tipe data yang akan kita pakai pada logika untuk memuat ulang
Sekarang, kita akan menulis modul klien OAuth2:
4.4 Kesimpulan
-
Tipe data aljabar (TDA) didefinisikan sebagai produk (
final case class
) dan ko-produk (sealed abstract class
). - Tipe
Refined
memperketat batasan pada nilai. - Fungsi konkret dapat didefinisikan pada sebuah
implicit class
agar alur pembacaan kode tetap dari kiri ke kanan. - Fungsi polimorfis didefinisikan pada kelas tipe. Fungsionalitas disediakan melalui batasan konteks “mempunyai”, bukan pada hierarki kelas “merupakan”.
- Instans kelas tipe merupakan implementasi dari kelas.
- Kelas tipe
@simulacrum.typeclass
menghasilkan.ops
pada pasangan dan menyediakan sintaks yang mudah pada fungsi di kelas tipe. - Derivasi kelas tipe merupakan komposisi yang dijalankan pada saat kompilasi atas instans kelas tipe.
5. Kelas Tipe Scalaz
Pada bab ini, kita akan melihat-lihat tipe kelas yang ada pada scalaz-core
.
Tentu kita tidak akan menggunakan semuanya pada drone-dynamic-agents
.
Maka dari itu, kita akan menggunakan contoh sederhana bila dibutuhkan.
Sebenarnya, banyak sekali kritik tentang penamaan pada Scala dan pemrograman
fungsional secara umum. Kebanyakan nama yang digunakan, menggunakan konvensi
yang dikenalkan oleh bahasa pemrograman Haskell berdasarkan Teori Kategori.
Silakan menggunakan tipe alias bila kata kerja yang melandasi fungsionalitas
utama lebih mudah diingat saat belajar. (Mis, Mappable
untuk yang bisa dipetakan,
Pureable
untuk yang bisa “diangkat”, dll).
Sebelum kita berbincang mengenai hierarki kelas tipe, kita akan melihat 4 metoda yang paling penting, bila dilihat dari sudut pandang kontrol alur
Kelas Tipe | Metoda | Dari | Diberikan | Untuk |
---|---|---|---|---|
Functor |
map |
F[A] |
A => B |
F[B] |
Applicative |
pure |
A |
F[A] |
|
Monad |
flatMap |
F[A] |
A => F[B] |
F[B] |
Traverse |
sequence |
F[G[A]] |
G[F[A]] |
Sebagaimana yang kita tahu bahwa operasi-operasi yang mengembalikan sebuah F[_]
dapat dijalankan secara berurutan pada komprehensi for
dengan memanggil
.flatMap
yang didefinisikan pada Monad[F]
terkait.
Konteks F[_]
bisa dianggap sebagai kontainer untuk efek intensional
dengan A
sebagai output: flatMap
memberikan kita jalan untuk menghasilkan
efek F[B]
pada saat waktu jalan berdasarkan hasil dari evaluasi efek sebelumnya.
Dan sudah barang tentu tidak semua konstruktor F[_]
mempunyai efek.
Bahkan, bila konstruktor tersebut mempunyai instans Monad[F]
, juga
belum tentu konstruktor tadi mempunyai efek.
Seringkali, konstruktor tersebut hanya merupakan struktur data yang
digunakan untuk pengabstraksian. Misalkan, kita bisa menggunakan List
,
Either
, Future
, dan lain lain untuk membuat struktur data.
Bila kita hanya perlu mengubah output dari sebuah F[_]
, maka kita
bisa menggunakan map
yang diperkenalkan oleh Functor
.
Pada bab 3, kita menjalankan banyak efek secara paralel dengan membuat
sebuah produk dan melakukan pemetaan (“mapping”) kepada produk tersebut.
Pada pemrograman fungsional, komputasi yang bisa diparalelkan seringkali
dianggap kurang manjur bila dibandingkan dengan komputasi sekuensial.
Di antara Monad
dan Functor
ada Applicative
yang mendefinisikan
pure
. pure
sendiri berfungsi untuk mengumpil sebuah nilai menjadi
sebuah efek ataupun membuat sebuah struktur data dari sebuah nilai tunggal.
Untuk .sequence
, metoda ini paling manjur bila digunakan untuk menyusun-ulang
konstruktor tipe. Bilamana kita mempunyai sebuah F[G[_]]
namun kita butuh G[F[_]]
, (tfw no gf)
adalah sebuah tindakan yang bijak bila kita menggunakan .sequence
.
Sebagai contoh, List[Future[Int]]
bisa diubah menjadi Future[List[Int]]
dengan
memanggil metoda tadi.
5.1 Agenda
Bab ini jauh lebih panjang dan padat informasi bila dibandingkan dengan bab lain. Kami sangat menyarankan pembaca nan budiman untuk membaca bab ini dalam beberapa kesempatan. Selain itu, juga disarankan untuk menganggap bab ini sebagai lumbung pencarian informasi lebih lanjut, tidak untuk mengingat-ingat.
Dan sebuah hal yang tidak mengejutkan bahwa kelas tipe yang memperpanjang
Monad
tidak dibahas pada bab ini karena kelas tipe tersebut akan dibahas pada
bab tersendiri.
Sebagai pengingat, Scalaz menggunakan pembuatan kode, bukan tiruan. Namun,
jangan kuatir, kita hanya akan menempelkan potongan kode dengan @typeclass
dengan alasan keringkasan. Sintaks yang ekuivalen juga tersedia ketika
kita meng-import scalaz._, Scalaz._
. Lebih tepatnya, ada pada paket
scalaz.syntax
pada sumber kode scalaz.
5.2 Yang dapat Dibubuhkan
Sebuah Semigroup
bisa didefinisikan sebagai sebuah tipe bila dua buah nilai
bisa digabungkan. Operasi penggabungan tersebut harus asosiatif yang berarti
urutan dari operasi berlapis tidak boleh berpengaruh.
Sebuah Monoid
merupakan sebuah Semigroup
dengan elemen “zero” / nol
(juga dikenal dengan elemen kosong atau identitas). Penggabungan zero
dengan sebuah nilai a
harus menghasilkan a
.
Pembicaraan ini membuat kita teringat tentang kennagan atas Numeric
pada bab 4.
Semua angka primitif mempunyai implementasi Monoid
. Namun, konsep “appendable” /
bisa dibubuhkan berguna tidak hanya untuk angka saja.
Ada hukum yang membatasi perilaku dari operasi append
pada kelas tipe
Band
, salah satunya adalah penambahan dari dua elemen harus idempoten.
Idempoten yang dimaksud disini adalah selalu memberikan nilai yang sama.
Contoh yang jamak digunakan misalkan Unit
, yang hanya mempunyai satu
nilai saja. Set
juga bisa digunakan. Walaupun Band
tidak mempunyai
metoda lain, pengguna dapat memanfaatkan properti ini untuk optimisasi
performa.
Sebuah contoh yang cukup realistis untuk Monoid
adalah mengenai sebuah sistem
trading yang mempunyai basis data templat jual beli yang sangat besar.
Untuk mengisi nilai bawaan dari sebuah trade, diperlukan pemilahan dan
penggabungan dari banyak templat dengan aturan “aturan terbaru yang dipakai”
bila ada dua templat sama sama menyediakan sebuah nilai untuk bidang yang sama.
Proses pemilahan sendiri sudah dilakukan oleh sistem lain. Tugas kitalah
yang menggabungkan templat templat tersebut.
Kita akan membuat skema templat sederhana untuk menunjukkan prinsip penggunaan monad. Harap diingat bahwa sebuah sistem yang realistis tentu mempunyai tipe data aljabar yang jauh lebih kompleks.
Bila kita menulis sebuah metoda yang menerima templates: List[TradeTemplate]
,
kita hanya perlu memanggil
dan selesai.
Tetapi, untuk bisa menggunakan zero
atau memanggil |+|
, kita harus
mempunyai instans Monoid[TradeTemplate]
. Walaupun kita bisa menurunkan
instans ini secara otomatis, seperti yang akan ditunjukkan pada bab
selanjutnya, demi contoh yang komprehensif, kita akan membuat instans
secara manual pada objek pasangan:
Yang disayangkan dari contoh di atas adalah Monoid[Option[A]]
akan menambah
konten dari A
. Bisa dilihat dari hasil REPL berikut:
sedangkan, yang kita inginkan adalah “yang digunakan adalah aturan terakhir”.
Kita dapat mengesampingkan nilai bawaan Monoid[Option[A]]
dengan mengganti
dengan kode berikut:
Dan semua terkompil dengan baik.
Yang kita butuhkan hanyalah pengimplementasian sebuah logika bisnis
dan Monoid
menyelesaikan semuanya.
Harap diperhatikan bahwa daftar payments
digabungkan. Kenapa demikian?
Karena perilaku bawaan dari Monoid[List]
adalah menggabungkan elemen-elemen
dan kebetulan perilaku tersebut juga kita harapkan. Bila persyaratan bisnis
yang ditemui berbeda, maka kita hanya perlu menyediakan Monoid[List[LocalDate]]
yang kita tulis sendiri. Dan jangan lupa bahwa dengan menggunakan polimorfisme
saat kompilasi, kita bisa mendapatkan implementasi append
yang berbeda
sesuai dengan E
pada List[E]
.
5.3 Semacam Objek
Pada bab mengenai Data dan Fungsionalitas, kita secara sekilas menyimpulkan
mengenai gagasan JVM atas persamaan menghancurkan banyak hal yang bisa
kita masukkan ke dalam sebuah ADT. Permasalahan mendasar mengenai hal ini
adalah JVM didesain untuk Java. Dan Java sendiri, equals
didefinisikan
pada java.lang.Object
. Hal ini diperparah dengan kenyataan bahwa metoda
tersebut tidak bisa dihapus dan tidak ada jaminan bahwa pasti sudah diimplementasikan.
Untungnya, pada pemprograman fungsional, kita lebih memilih untuk menggunakan kelas tipe untuk memenuhi kebutuhun atas fungsionalitas polimorfis. Ditambah dengan konsep persamaan diperiksa pada saat waktu kompilasi.
Dan faktanya, ===
adalah lebih aman dibandingkan ==
yang hanya bisa
dikompilasi bila tipe dari kedua belah sisi operator ini mempunyai tipe
yang sama. Kita bisa mengatakan bahwa operator ini salah satu jaring
pengaman yang bagus.
equal
mempunyai persyaratan implementasi yang sama dengan Object.equals
-
komutatif
f1 === f2
yang juga sama denganf2 === f1
-
refleksif
f === f
-
tarnsitif
f1 === f2 && f2 === f3
yang juga sama denganf1 === f3
Dengan membuang konsep umum Object.equals
, kita tidak akan menyia-nyiakan
persamaan saat kita menyusun sebuah ADT. Selain itu, kita tak akan mendapatkan
harapan palsu akan persamaan dimana sebenarnya tidak pernah ada.
Melanjutkan tren pengubahan konsep Java, data tidak lagi merupakan
java.lang.Comparable
namun memiliki Order
, berdasarkan:
Order
mengimplementasikan .equal
dalam primitif baru .order
. Ketika
sebuah kelas tipe mengimplementasikan kombinator primitif bapaknya dengan
sebuah kombinator turunan, maka hukum substitusi untuk kelas
tipe tersebut akan ditambahkan secara tidak langsung. Bila sebuah instans
dari Order
digunakan untuk mengesampingkan .equal
dengan alasan performa,
maka instans tersebut harus punya perilaku yang identik dengan implementasi
yang asli.
Things that have an order may also be discrete, allowing us to walk successors and predecessors:
Objek-objek yang mempunyai order bisa jadi juga merupakan objek diskrit, hal ini memberikan kita ruang untuk memeriksa objek sebelum dan sesudahnya:
Kita akan berdiskusi mengenai EphemeralStream
pada bab berikutnya. Untuk
saat ini, kita hanya perlu tahu bahwa tipe data ini bisa digunakan untuk menyusun
struktur data tak hingga tanpa harus kuatir mengenai masalah memori sebagaimana
struktur data Stream
dari pustaka standar.
Sama halnya dengan Object.equals
, konsep .toString
yang ada pada tiap kelas
sangat tidak masuk akal di Java. Idealnya, kita harus memastikan bahwa sebuah
objek memang bisa diubah menjadi string pada saat waktu kompilasi.
Untuk mendapatkan hasil tersebut, kita dapat menggunakan Show
:
Kita akan membahas Cord
lebih mendetail pada bab selanjutnya mengenai tipe data.
Untuk saat ini, kita hanya perlu tahu bahwa Cord
merupakan struktur data yang
efisien yang dipergunakan untuk menyimpan dan memanipulasi String
.
5.4 Yang Dapat Dipetakan
Kita akan fokus pada benda benda yang bisa dipetakan atau dilalui:
5.4.1 Fungtor
Satu-satunya metoda abstrak adalah map
yang harus bisa menggabungkan
dua fungsi. Sebagai contoh, memetakan f
dan dilanjutkan dengan g
sama dengan memetakan dengan hasil komposisi dari f
dan g
:
map
juga harus melakukan no-op bila fungsi yang disediakan berupa
fungsi identity
(x => x
).
Functor
mendefinisikan beberapa metoda pembantu untuk map
yang bisa dioptimalkan
dengan instans khusus. Dokumentasi memang sengaja dihilangkan pada definisi diatas
agar pembaca budiman memnebak apa yang sebuah metoda lakukan sebelum melihat
ke implementasi dari metoda tersebut. Sangat disarankan untuk memperhatikan dengan
penanda tipe dengan seksama berikut sebelum melanjutkan seksi ini:
-
void
menerima sebuah instans dariF[A]
dan selalu mengembalikanF[Unit]
. Metoda ini selalu menghapus semua nilai sembari menjaga struktur. -
fproduct
menerima input yang sama denganmap
namun mengembalikanF[(A, B)]
. Sebagai contoh, fungsi ini akan memasangkan konten dengan hasil dari fungsi tersebut. Fungsi ini berguna bila kita ingin tetap menggunakan input yang diterima oleh fungsi ini. -
fpair
menggandakan semua elemen dariA
menjadi tupleF[(A, A)]
. -
strengthL
memasangkan konten dari sebuahF[A]
dengan konstanB
pada bagian kiri. -
strengthR
memasangkan konten dari sebuahF[A]
dengan konstanB
pada bagian kanan. -
lift
menerima sebuah fungsiA => B
dan mengembalikanF[A] => F[B]
. Dengan kata lain, fungsi ini menerima sebuah fungsi berdasarkan konten dariF[A]
dan mengembalikan sebuah fungsi yang beroperasi secara langsung padaF[A]
. -
mapply
sendiri merupakan fungsi yang agak janggal. Misalkan kita mempunyai sebuahF[_]
pada fungsiA => B
dan nilaiA
. Kita bisa mendapatkan hasil berupaF[B]
. Fungsi ini mempunyai signature yang mirip denganpure
, namun mengharuskan pemanggil fungsi ini untuk mempunyaiF[A => B]
.
Secara sekilas, fpair
, strengthL
, dan strengthR
terlihat tidak berguna.
Namun, kita bisa menggunakannya saat kita ingin tetap menggunakan informasi
yang bisa jadi hilang saat keluar dari cakupan fungsi. Misal, indeks dari
sebuah List
atau Set
saat melakukan traverse
.
Functor
punya beberapa sintaks khusus, antara lain:
.as
dan >|
digunakan untuk mengganti keluaran fungsi dengan sebuah
konstanta.
Pada contoh aplikasi kita, terdapat sebuah tambalan yang tidak kita
ungkap sampai sekarang. Tambalan tersebut adalah pendefinisian start
dan stop
untuk mengembalikan input:
Pendefinisian diatas memperkenankan kita untuk menulis logika bisnis yang ringkas seperti
dan
Namun, tambalan ini melimpahkan kompleksitas ke bagian implementasi secara mubazir.
Sungguh, jauh lebih disukai bila kita mendesain aljabar kita untuk
mengembalikan F[Unit]
dan menggunakan as
:
dan
5.4.2 Foldable
Secara teknis, Foldable
merupakan struktur data yang bisa langkahi satu-per-satu
untuk menghasilkan sebuah nilai ijmal. Namun, terlalu meremehkan bila kita hanya
berhenti sampai disitu. Kenyataannya, Foldable
merupakan kelas tipe yang
bisa menjawab hampir semua apa yang diharapkan dari sebuah Koleksi APA.
Berhubung kelas tipe ini mempunyai begitu banyak metoda, ada baiknya kita pecah pecah. Dimulai dengan metoda abstrak:
Secara teori, untuk mendapatkan semua fungsionalitas dari kelas tipe Foldable
,
sebuah instans hanya perlu mengimplementasikan foldMap
dan foldRight
.
Walaupun pada kenyataannya, banyak sekali metoda lain yang diimplementasikan
sesuai dengan struktur data yang dibutuhkan agar waktu jalan lebih optimal.
Di pasaran, santer terdengar istilah keren MapReduce. Pada Scala
sendiri, istilah tersebut dikenal dengan .foldMap
. .foldMap
sendiri
berupa fungsi yang hanya membutuhkan fungsi yang memetakan A
ke B
,
sebuah F[A]
, dan cara untuk menggabungkan semua hasil pemetaan dari
A
ke B
menjadi satu nilai (yang disediakan oleh Monoid
dan zero
dari B
)
untuk menghasilkan nilai ijmal B
. Selain itu, tidak ada urutan operasi
dari fungsi yang memetakan A
ke B
. Sehingga, memungkinkan untuk
dilakukannya komputasi paralel.
Untuk foldRight
, metoda ini tidak memaksa parameternya untuk mempunyai
instans Monoid
walau harus menerima sebuah nilai awal z
dan cara
penggabungan tiap elemen dari struktur data. Selain itu, urutan pelangkahan
dari elemen elemen input adalah dari kiri ke kanan. Hal ini juga berarti
bahwa metoda ini tidak bisa dijalankan secara paralel.
foldLeft
melangkahi semua elemen dari kiri ke kanan. Untuk mengimplementasikan
foldLeft
, kita bisa menggunakan foldMap
walau kebanyakan instans
juga mengimplementasikan sendiri foldLeft
khusus untuk instans tersebut.
Hal lain yang patut diperhatikan adalah implementasi metoda ini berupa
rekursi akhir, foldLeft
tidak menggunakan parameter panggilan.
Hukum yang mengatur Foldable
hanya ada satu, yaitu foldLeft
dan
foldRight
harus konsisten dengan foldMap
untuk operasi monoidal.
Misalnya, menambahkan sebuah elemen di bagian awal dari sebuah senarai
untuk implmentasi foldLeft
dan menambahkan sebuah elemen di bagian
akhir dari sebuah senarai untuk foldRight
.
Di sisi lain, foldLeft
dan foldRight
tidak harus selalu konsisten
satu sama lain. Bahkan, seringkali mereka mempunyai hasil yang berlawanan.
Hal yang paling sederhana untuk dilakukan pada foldMap
adalah menggunakan
fungsi identity
yang menghasilkan fold
(ijmal natural dari elemen monoidal)
dengan varian kiri/kanan agar dapat memperkenankan pemilihan berdasarkan
kriteria performa:
Mengulang apa yang kita pelajari tentang Monoid
, kita menulis:
Namun, kode di atas bisa ditulis ulang menjadi
Sayangnya, .fold
tidak bisa dipanggil dari List
milik pustaka standar.
Hal ini dikarenakan List
sudah memiliki metoda dengan nama fold
yang
berbeda dengan metoda fold
dari kelas tipe Foldable
.
Untuk metoda intercalate
, metoda ini menyisipkan sebuah A
spesifik
diantara elemen sebelum melakukan operasi fold
metoda ini bisa dianggap sebagai metoda mkString
dari pustaka standar
yang diumumkan:
foldLeft
menyediakan kita cara untuk mendapatkan elemen manapun dengan
melangkahi semua elemen satu per satu, termasuk dengan beberapa metoda
lainnya:
Berbeda dengan List(0)
yang sangat mungkin melempar eksepsi, Foldable.index
lebih memilih untuk mengembalikan sebuah Option[A]
atau bisa juga dengan
mengembalikan sebuah A
bila menggunakan .indexOr
(dengan sebuah nilai
bawaan A
). Sama halnya dengan .contains
punya pustaka standar yang menggunakan
persamaan standar dari JVM, Scalaz menyediakan .element
yang menggunakan Equal
yang jauh lebih unggul.
Metoda ini sangat terlihat mirip dengan APA koleksi. Dan yang paling utama,
Foldable
bisa diubah menjadi List
.
Selain itu, konversi ke tipe data lain juga ada. Sebagai contoh, .toStream
,
.toSet
, .toVector
, .to[T <: TraversableLike]
, dan lain sebagainya.
Untuk pengecekan predikat, Foldable menyediakan
Untuk memeriksa jumlah elemen yang bernilai true
terhadap sebuah predikat,
kita bisa menggunakan filterLength
. Sedangkan untuk memeriksa apakah
semua elemen bernilai true
, all
adalah fungsi yang tepat guna.
any
sendiri hanya memastikan bahwa setidaknya ada satu elemen dari Foldable
bernilai true
terhadap predikat yang disediakan.
Untuk memecah sebuah F[A]
menjadi beberapa bagian, kita bisa menggunakan
splitBy
sebagai contoh
patut diperhatikan bahwa ada dua nilai dengan indeks 'b'
.
Bilamana sebuah senarai objek A
yang tidak memiliki kelas tipe Equal
namun kita ingin memecahnya menjadi beberapa bagian, kita bisa menggunakan
splitByRelation
yang meminta operator pembanding sebagai gantinya.
Bisa juga kita memecah sebuah Foldable
menjadi dua bagian, satu bagian
memenuhi sebuah predikat, dan sebaliknya, dengan menggunakan splitWith
.
Sedangkan untuk memilih himpunan yang memenuhi predikat sembari membuang
yang lain, kita menggunakan selectSplit
.
Untuk findLeft
dan findRight
sendiri, metoda ini mengambil elemen
pertama dari kiri atau kanan yang sesuai dengan predikat.
Dengan menggunakan Equal
dan Order
, kita juga mendapat metoda lain
yang mengembalikan himpunan.
distinct
, secara pengimplementasian, lebih efisien bila dibandingkan
dengan distinctE
. Hal ini disebabkan karena distinct
menggunakan
pengurutan (menggunakan Order
) sehingga menggunakan algoritma yang
mirip dengan quicksort yang relatif lebih cepat bila dibandingkan dengan
menggunakan List.distinct
dari pustaka standar. Keuntungan lain
adalah struktur data semacam set secara otomatis mempunyai distinct
.
Untuk mengelompokkan berdasarkan dari hasi sebuah fungsi atas tiap elemen,
kita bisa menggunakan distinctBy
. Sebagai contoh, kita bisa mengelompokkan
nama berdasarkan huruf pertama.
Kita dapat menggunakan Order
lebih lanjut untuk mengekstrak elemen dengan
nilai terkecil maupun terbesar dari sebuah Foldable
. Lebih lanjut lagi,
kita juga akan menggunakan pola varian Of
dan By
untuk memetakan
elemen-elemen tadi ke tipe lain ataupun menggunakan tipe lain sebagai
pembanding urutan.
Sebagai contoh, kita bisa memeriksa String
manakah mempunyai nilai
paling besar berdasarkan panjang dengan menggunakan varian By
.
Bisa juga kita mencari nilai paling besar dari elemen-elemen yang ada
dengan menggunakan varian Of
.
Dengan ini, fitur utama dari Foldable
sudah digambarkan secara sekilas.
Dan hikmah yang bisa kita ambil dari sub-bab ini adalah apapun yang bisa
kita gunakan pada pustaka collection, kita juga bisa mendapatkannya
pada Foldable
.
Kita akan menutup sub-bab ini dengan beberapa variasi metoda yang sudah
kita lihat sebelumnya. Pertama, berikut adalah metoda yang menerima
subah Semigroup
, bukan Monoid
:
yang mengembalikan Option
dengan pertimbangan struktur data yang kosong.
(Harap ingat, Semigroup
tidak mempunyai zero
)
Kelas tipe Foldable1
berisi jauh lebih banyak varian Semigroup
dari
metoda Monoid
bila dibandingkan dengan yang ditampilkan di sini.
Dan hal itu dirasa masuk akal untuk struktur data yang tidak bisa kosong,
tanpa harus memaksa elemen elemennya mempunyai kelas Monoid
.
Tidak kalah penting, ada beberapa varian yang menerima nilai nilai kembalian monadik.
Kita juga telah menggunakan foldLeftM
saat kita menulis logika bisnis dari
aplikasi kita.
Sekarang, kita tahu dari mana asal fungsi tersebut.
5.4.3 Traverse
Traverse
bisa dikatakan sebagai penggabungan antara Functor
dan Foldable
Pada awal bab, kita telah menunjukkan mengenai pentingnya traverse
dan
sequence
untuk membolak-balik konstruktor tipe agar sesuai dengan requirement.
Sebagai contoh, List[Future[_]]
menjadi Future[List[_]]
.
Tidak seperti Foldable
dimana kita tidak dapat serta merta mengasumsikan
bahwa reverse
adalah sebuah hak asasi, dengan Traverse
kita dapat
dengan santai membolak-balik sesuatu.
Selain itu, kita juga bisa merekatkan dua buah objek yang mempunyai Traverse
menjadi F[(A, B)]
. Namun, harap diingat bahwa ada kemungkinan bahwa panjang
kedua benda tadi tidak sama, sehingga kita harus menggunakan Option[A]
atau
Option[B]
bilamana panjang salah satu benda lebih pendek bila dibandingkan
dengan panjang benda lainnya. Untuk menangani hal tersebut, kita bisa menggunakan
zipL
maupun zipR
untuk menentukan sisi mana yang “dipotong” bila panjang
berbeda. zip
merupakan fungsi khusus untuk menambahkan sebuah indeks untuk
setiap entri yang terindeks.
zipWithL
dan zipWithR
memberikan kita kesempatan untuk membuat sebuah F[C]
dengan menggabungkan kedua sisi dari panggabungan tadi.
mapAccumR
dan mapAccumL
sebenarnya hanya map
yang dikombinasi dengan
sebuah akumulator. Bilamana kita terbiasa dengan Java yang membuat kita
ingin menggunakan sebuah var
dan menggunakan var
tersebut dalam sebuah
map
, maka kita harus menggunakan mapAccumL
.
Sebagai contoh, anggap saja kita mempunyai sebuah senarai kata dan kita ingin mengabaikan kata-kata yang sudah ada. Algoritma penyaringan tidak diperkenankan untuk mengolah senarai kata dua kali. Keuntungan yang didapat adalah algoritma ini bisa mencapai urutan tak hingga:
Pada akhirnya, Traverse1
, sebagaimana Foldable1
, menyediakan varian metoda
metoda untuk struktur data yang tidak bisa kosong. Selain itu, Traverse1
juga
menerima Semigroup
, bukan Monoid
, dan sebuah Apply
, bukan Applicative
.
Harap diingat bahwa Semigroup
tidak mempunyai .empty
dan Apply
tidak harus
mempunyai .point
.
5.4.4 Align
Untuk bab ini, kita berbicara tentang Align
. Align
sendiri juga berbicara
mengenai penggabungan pelapisan Functor
. Ada baiknya sebelum kita menyelami
Align
, kita memandang sekilas mengenai tipe data \&/
yang akan kita
panggil dengan hore!
bisa dibilang, hore! merupakan penyandian data logika inklusif OR
.
A
, B
, ataupun keduanya. Jadi, bilamana ada token F[A \&/ B]
,
kita bisa menafsirkannya sebagai “sebuah fungtor yang bisa berisi A
ataupun B
.”
alignWith
menerima sebuah fungsi yang menghasilkan C
, bisa dari A
,
B
, ataupun keduanya, dan mengembalikan sebuah fungsi yang terangkat
dari tuple F[A]
dan F[B]
menjadi F[C]
. Sedangkan bila kita ingin
membuat sebuah fungtor hore!, kita bisa menggunakan align
yang
membuat sebuah \&/
dari dua F[_]
.
merge
memberikan kita jalan untuk menggabungkan dua F[A]
bila A
mempunyai Semigroup
. Sebagai contoh, implementasi dari Semigroup[Map[K, V]]
mengikuti implementasi dari Semigroup[V]
dan menggabungkan dua entri.
Selain mengembalikan nilai penggabungkan, implmentasi ini juga berperilaku
sebagaimana sebuah multimap:
dan ketika penggabungan terjadi, Map[K, Int]
hanya perlu menambahkan
isi dari map tersebut:
.pad
dan .padWith
biasa digunakan untuk menggabungkan dua struktur data,
yang munggkin saja tidak lengkap pada salah satunya, secara parsial.
Sebagai contoh, kita menggunakan fungsi ini saat kita ingin mengagregasi
penghitungan suara independen dan tetap menyimpan asal dari suara tersebut.
Ada beberapa varian dari align
yang memudahkan kita untuk menggunakan
struktur dari \&/
yang seharusnya bisa terlihat dari penanda tipe mereka. Contoh:
Harap dicatat bahwa varian A
dan B
menggunakan inklusif OR
sedangkan
varian This
dan That
menggunakan ekslusif OR
yang mengembalikan None
bila nilai pada salah satu sisi.
5.5 Variance
Mungkin adalah sebuah keputusan yang tepat bila kita kembali membahas
Functor
sesaat dan mendiskusikan hierarki yang sebelumnya kita abaikan:
InvariantFunctor
, yang juga dikenal sebagai fungtor eksponensial,
mempunyai metoda xmap
yang menyatakan bahwa bila kita mempunyai fungsi
yang memetakan A
ke B
dan sebuah fungsi yang memetakan B
ke A
,
maka kita dapat mengkonversi F[A]
ke F[B]
.
Functor
merupakan kependekan dari yang seharusnya disebut fungtor kovarian.
Dikarenakan Functor
sudah lebih dulu dikenal, maka penyebutan ini
diteruskan. Begitu halnya dengan Contravariant
, fungtor ini seharusnya
disebut sebagai fungtor kontravarian.
Functor
mengimplementasikan xmap
dengan map
dan mengabaikan fungsi
dari B
ke A
. Sedangkan Contravariant
mengimplementasikan xmap
dengan contramap
dan mengabaikan fungsi dari A
ke B
:
Adalah hal yang penting untuk diperhatikan, walaupun secara teori berkaitan,
kata kovarian, kontravarian, dan invarian tidak berhubungan secara
langsung dengan varian tipe punya Scala (mis, +
dan -
yang biasa ditulis
pada penanda tipe). Invarian yang dimaksudkan disini
adalah bisa dilakukannya pemetaan konten dari struktur F[A]
ke F[B]
.
Menggunakan identity
, kita dapat menentukan bahwa A
bisa dengan aman
di-downcast atau upcast menjadi B
dengan melihat varian dari
fungtor.
.map
bisa dipahami dengan “bila kamu punya sebuah F
atas A
dan cara untuk
mengubah A
ke B
, maka saya bisa memberi kamu sebuah F
atas B
.”
Sebaliknya, .contramap
dapat dibaca sebagai “bila kamu mempunyai F
atas A
dan cara untuk mengubah B
menjadi A
, maka saya dapat memberi kamu sebuah F
atas B
.”
Anggap contoh berikut: pada aplikasi kita, kita memperkenalkan tipe spesifik domain
Alpha
, Beta
, Gamma
, dan lain lain untuk memastikan bahwa kita tidak akan
mencampur aduk angka angka pada kalkulasi finansial:
namun, masalah diatas tergantikan dengan masalah baru mengenai tidak adanya
kelas tipe untuk tipe baru ini. Bilamana kita menggunakan nilai pada dokumen
JSON, kita harus menulis instans dari JsEncoder
dan JsDecoder
untuk tipe
baru tadi.
Untungnya, JsEncoder
mempunyai sebuah Contravariant
dan JsDecoder
mempunyai
sebuah Functor
sehingga kita dapat menurunkan instans tersebut dengan mengisi
kontrak:
- “bila kamu memberi saya sebuah
JsDecoder
untukDouble
dan cara untuk mengubahDouble
menjadiAlpha
, maka saya akan memberikan sebuahJsDecoder
untukAlpha
.” - “bila kamu memberi saya sebuah
JsEncoder
untukDouble
dan cara untuk mengubahAlpha
menjadiDouble
, maka saya akan memberikan sebuahJsEncoder
untukAlpha
.”
Metoda pada kelas tipe bisa saja mempunyai tipe parameter dengan posisi kontravarian
(parameter metoda) atau posisi kovarian (tipe kembalian). Bila sebuah kelas tipe
mempunyai sebuah kombinasi atas posisi kovarian dan kontravarian, bisa jadi
kelas tipe tersebut mempunyai fungtor invarian. Sebagai contoh, Semigroup
dan Monoid
mempunyai InvariantFunctor
namun tidak memiliki Functor
maupun
Contravariant
.
5.6 Apply dan Bind
Sub-bab ini bisa dianggap sebagai pemanasan untuk Applicative
dan Monad
5.6.1 Apply
Apply
memperpanjang Functor
dengan menambahkan sebuah metoda dengan
nama ap
yang mirip dengan map
, dalam batasan ap
juga menerapkan
sebuah fungsi ke nilai. Bedanya, fungsi yang diterima ap
masih dalam
konteks yang sama dengan nilai yang diterapi.
Implikasi dari hal ini adalah struktur data sederhana seperti Option[A]
juga mempunyai implementasi .ap
Untuk mengimplementasikan .ap
, pertama-tama kita harus mengekstrak
fungsi ff: A => B
dari f: Option[A => B]
, dan dilanjutkan dengan memetakan
ff
atas fa
. Ekstraksi fungsi dari konteks adalah fitur penting dari Apply
yang memberikan ruang untuk menggabungkan isi dari konteks yang melingkupi
operasi pemanggilan ap
.
Kembali ke Apply
, kita menemukan plat cetak .applyX
yang
menyediakan jalan untuk menggabungkan fungsi-fungsi paralel dan pada akhirnya
memetakan nilai keluaran mereka:
.apply2
bisa dibaca sebagai: “bila kamu memberi saya sebuah F
atas A
dan
F
atas B
dan sebuah cara untuk menggabungkan A
dan B
menjadi C
, maka
saya akan memberi kamu F
atas C
.” Ada beberapa penggunaan atas metoda ini.
Dan, dua yang paling penting adalah:
- membuat kelas tipe untuk tipe produk
C
dariA
danB
- melakukan efek secara paralel, sebagaimana drone dan aljabar google yang kita buat pada Bab 3, dan menggabungkan hasilnya.
Sudah barang tentu Apply
mempunyai beberapa sintaks khusus yang
berguna:
yang sudah kita gunakan pada Bab 3:
Sintaks <*
dan *>
(paruh buruh kiri dan kanan) menawarkan cara mudah untuk
mengabaikan keluaran dari salah satu dari dua efek paralel .
Walaupun sintaks |@|
cukup jelas, ada masalah yang ada pada ApplicativeBuilder
.
Yaitu, pengalokasian objek dengan instans ApplicativeBuilder
baru tiap kali
penambahan efek. Bila tugas yang diberikan sangat bergantung pada I/O, maka
alokasi memori tidak signifikan. Namun, bila tugas sangat bergantung pada
CPU, maka sangat disarankan untuk menggunakan sintaks alternatif pengangkatan dengan arity
yang tidak membuat objek penengah.
digunakan seperti
atau memanggil applyX
secara langsung
Walaupun lebih sering digunakan bersama dengan efek, Apply
juga bisa digunakan
dengan struktur data. Misalkan, kita bisa menulis ulang
sebagai
Bila kita hanya ingin menggabungkan keluaran sebagai sebuah tuple, ada metoda yang bisa memenuhi hal tersebut:
Juga ada versi umum dari ap
untuk lebih dari dua parameter:
yang bersamaan dengan metoda .lift
yang menerima fungsi normal dan mengangkat
mereka pada konteks F[_]
juga ada pula aplikasi sintaks parsial untuk ap
Dan terakhir, .forever
yang mengulang operasi dengan efek tanpa henti. Instans dari Apply
harus aman
secara alokasi stack atau kita bisa mendapatkan galat StackOverflowError
.
5.6.2 Bind
Fungsi utama yang dibawa oleh Bind
tentu adalah .bind
yang sama dan sebangun
dengan .flatMap
. Dan sebagaimana yang telah kita pelajari pada bab sebelumnya,
fungsi ini, .bind
, memperkenankan sebuah fungsi untuk menerima nilai input dari
keluaran dari fungsi dengan efek, dan pada akhirnya fungsi .bind
mengembalikan
sebuah nilai dengan efek yang sama dari fungsi pemberi nilai input.
Fungsi .bind
ini juga bisa menggabungkan dua buah struktur data.
Pembaca budiman yang biasa menggunakan fungsi .flatten
dari pustaka standar
mungkin akan merasa familiar dengan fungsi .join
. .join
menerima sebuah
konteks yang berlapis dan meratakan lapisan lapisan tadi menjadi satu.
Untuk kombinator turunan yang ada pada kelas tipe ini, pembaca budiman mendapatkan
.ap
dan .apply2
yang mempunyai batasan untuk selalu berkesesuaian dengan .bind
.
Pada nantinya, kita akan menyaksikan bahwa hukum ini berpengaruh besar dalam
strategi paralelisasi.
Sebagaimana halnya dengan Functor.fproduct
, mproduct
juga memasangkan
masukan dan keluaran dari fungsi tersebut di dalam F
.
Bilamana if
merupakan konstruk kondisional, ifM
merupakan konstruk
kondisional yang menerima struktur data atau operasi dengan efek:
Untuk masalah performa, pembaca budiman tidak perlu kuatir bila menggunakan
ifM
dan ap
karena kedua fungsi ini sudah dioptimalkan untuk menyimpan
hasil eksekusi cabang kode di tembolok dan menggunakannya kembali saat
kondisi terpenuhi. Sebagai contoh, silakan perhatikan contoh berikut
yang menciptakan objek List(0)
atau List(1, 1)
baru tiap kali percabangan
dieksekusi.
Untuk pembaca yang menyukai operator sintaks, Bind
juga menyediakan
sintaks khusus.
Operator >>
biasa digunakan bila kita ingin membuang masukan bind
.
Sebaliknya, >>!
digunakan ketika kita ingin menjalankan sebuah efek
dan membuang keluarannya.
5.7 Applicative dan Monad
Bila dipandang dari sudut pandang fungsionalitas, Applicative
merupakan
Apply
dengan metoda pure
. Kurang lebih hal yang sama dengan Monad
,
merupakan Applicative
yang diperluas dengan menggabungkan Bind
.
Setelah mempertimbangkan banyak hal, Applicative
dan Monad
bisa dianggap sebagai
puncak atas semua yang telah kita pelajari dari bab ini.
Sebagai contoh, .pure
, atau .point
bagi struktur data, acap kali digunakan
untuk menghasilkan efek ataupun struktur data dari nilai.
Untuk membuat instans Applicative
, pembaca budiman harus menerapkan sifat-sifat
sebagai berikut:
-
Identity:
fa <*> pure(identity) === fa
, (wherefa
is anF[A]
) i.e. applyingpure(identity)
does nothing. -
Homomorphism:
pure(a) <*> pure(ab) === pure(ab(a))
(whereab
is anA => B
), i.e. applying apure
function to apure
value is the same as applying the function to the value and then usingpure
on the result. -
Interchange:
pure(a) <*> fab === fab <*> pure(f => f(a))
, (wherefab
is anF[A => B]
), i.e.pure
is a left and right identity -
Mappy:
map(fa)(f) === fa <*> pure(f)
-
Identitas:
fa <*> pure(identity) === fa
dimanafa
merupakan sebuahF[A]
. Sebagai contoh, pengaplikasianpure(identity)
harus tidak mempunyai efek apapun tanpa mengubah apapun. -
Homomorfisme:
pure(a) <*> pure(ab) === pure(ab(a))
dimanaab
merupakan pemeteaan dariA
keB
(A => B
). Misalkan, penerapan sebuah fungsipure
ke sebuah nilaipure
adalah sama dengan penerapan fungsi tersebut ke sebuah nilai yang sama dan dilanjutkan dengan menerapkan fungsipure
pada hasilnya. -
Komutatif:
pure(a) <*> fab == fab <*> pure (f => f(a))
dimanafab
merupakan sebuahF[A => B]
. Contoh yang paling sederhana dari sifat ini adalahpure
yang merupakan sebuah fungsi identitas baik untuk sisi kiri maupun sisi kanan. -
Mappy:
map(fa)(f) === fa <*> pure(f)
.
Dan sifat tambahan untuk Monad
diatas adalah:
-
Identitas Kiri:
pure(a).bind(f) === f(a)
. -
Identitas Kanan:
a.bind(pure(_)) === a
. -
Asosiatif:
fa.bind(f).bind(g) === fa.bind(a => f(a).bind(g))
dimanafa
merupkana sebuahF[A]
,f
merupakan sebuahA => F[B]
dang
merupakanB => F[C]
.
Sifat asosiatif menentukan bahwa pemanggilan fungsi bind
yang disambung harus
sesuai dengan fungsi bind
lainnya. Walaupun bukan berarti kita bisa dengan
seenaknya mengubah urutan pemanggilan fungsi fungsi tersebut karena hal tersebut
adalah sifat komutatif. Sebagai contoh, flatMap
yang merupakan alias dari bind
tidak dapat diubah dari
menjadi
karena start
dan stop
tidak bersifat komutatif. Tentu karena efek dari
kedua fungsi tersebut berbeda (bahkan berkebalikan!).
Berbeda halnya dengan penerapan sifat komutatif untuk pemanggilan beberapa start
maupun stop
. Sebagai contoh, kita dapat menulis ulang fungsi berikut
menjadi
yang, bila menggunakan dipandang menggunakan kacamata aljabar kita, setara. Tentu hal ini tidak bisa secara buta diterapkan ke semua aljabar. Lalu, kenapa kita melakukan hal ini? Karena kita mengasumsikan banyak hal dari Antarmuka Pemrograman Aplikasi dari Google Container yang kurang lebih cukup masuk akal dilakukan.
Konsekuensi praktis dari hal ini adalah sebuah Monad
harus bersifat komutatif
bila moteda applyX
dapat dijalankan secara paralel. Dan pada Bab 3, kita mengambil
jalan pintas saat kita menjalankan efek efek ini secara paralel
karena kita tahu bahwa fungsi-fungsi diatas bersifat komutatif bila dijalankan secara bebarengan. Bila nanti sudah waktunya untuk kita menerjemahkan aplikasi kita, kita harus membuktikan bahwa efek-efek yang dihasilkan oleh fungsi-fungsi diatas harus bersifat komutatif dan bila implementasi bersifat asinkronus, kita bisa saja mengubah operasi menjadi bersifat berurutan, untuk menghindari kejadian yang tidak diinginkan.
Mengenai seluk beluk tentang cara yang dianjurkan saat kita berurusan dengan pengurutan efek dan apa saja efek efek yang ada, akan dibahas pada bab khusus mengenai Monad Lanjutan.
5.8 Divide dan Conquer
Sebagaimana yang terlihat pada gambar di atas, Divide
berkorelasi
dengan Contravariant
sebagaimana Apply
berkorelasi dengan Functor
.
divide
menyatakan bahwa bila kita bisa memecah sebuah C
menjadi sebuah
A
dan B
, dan kita mendapat sebuah F[A]
dan F[B]
, maka kita bisa
mendapatkan seubah F[C]
.
Hal semacam ini sangat memudahkan kita untuk membuat instans kelas tipe
kontravarian untuk tipe produk dengan memecah produk-produk menjadi
bagian-bagian yang menyusunnya. Sebagai contoh, mari kita membuat sebuah
Equal
untuk tipe produk baru, Foo
, dari instans Divide[Equal]
milik
Scalaz
Sebagaimana dengan Apply
, Divide
juga mempunyai sintaks untuk tuple.
Berikut merupakan contoh memecah belah lalu menguasai dalah menyelesaikan
permasalah pada perangkat lunak:
Secara umum, bila kelas tipe penyandi mampu menyediakan sebuah instans
dari Divide
, dan tidak berhenti hanya pada Contravariant
, adalah
sebuah hal yang tidak mustahil untuk menurunkan instans untuk semua
case class
. Sama halnya dengan kelas tipe dekoder juga mampu menyediakan
instans Apply
. Pembahasan mengenai penurunan kelas tipe akan dibahas
lebih lanjut pada bab berikutnya.
Seperti yang sudah dibahas pada beberapa paragraf di atas, Divisible
ke
Contravariant
adalah sama halnya dengan Applicative
ke Functor
.
Selain itu, kelas tipe ini menyediakan metoda .conquer
yang sama dengan
.pure
.conquer
memperkenankan kita untuk membuat penerapan sederhana yang
mengabaikan parameter tipe. Nilai nilai tersebut biasa disebut sebagai
terkuantifikasi secara umum. Sebagai contoh,
Divisible[Equal].conquer[INil[String]]
akan mengembalikan sebuah
implementasi Equal
untuk senarai String
kosong yang akan selalu true
.
5.9 Plus
Plus
merupakan Semigroup
yang dikhususkan untuk konstruktor tipe.
Sedangkan PlusEmpty
adalah padanan untuk Monoid
. Untuk IsEmpty
lebih dikhususkan untuk menentukan apakah sebuah F[A]
kosong atau tidak.
Walaupun secara kasat mata <+>
berperilaku seperti |+|
alangkah baiknya untuk menganggap operator ini hanya beroperasi pada F[_]
dan
tidak melihat isi dari fungtor tersebut. Plus
juga mempunyai konvensi untuk
selalu menghiraukan galat dan mengambil hasil operasi pertama. Maka dari itu,
operator <+>
dapat digunakan sebagai mekanisme arus pendek dan penanganan galat
melalui gerakan mundur teratur:
Sebagai contoh, bila kita mempunyai NonEmptyList[Option[Int]]
dan kita ingin
menghiraukan nilai None
beserta mengambil hasil yang pertama kali munncul,
kita akan memanggil <+>
dari Foldable1.foldRight1
:
In fact, now that we know about Plus
, we realise that we didn’t need to break
typeclass coherence (when we defined a locally scoped Monoid[Option[A]]
) in
the section on Appendable Things. Our objective was to “pick the last winner”,
which is the same as “pick the winner” if the arguments are swapped. Note the
use of the TIE Interceptor for ccy
and otc
with arguments swapped.
Bahkan nyatanya, setelah kita tahu mengenai Plus
, kita akan menyadari bahwa
kita tidak perlu merusak koherensi kelas tipe (saat mendefinisikan
sebuah Monoid[Option[A]]
dengan cakupan lokal) pada seksi Appendable Things.
Tujuan kita adalah “mengambil hasil pertama yang ditemui” yang sama saja dengan
“mengambil hasil terakhir” bila argumen dibalik. Mohon diperhatikan, argumen
<+>
untuk ccy
dan otc
ditukar termpatnya.
Applicative
dan Monad
juga mempunyai versi khusus dari PlusEmpty
.unite
memperkenankan kita untuk menekuk struktur data menggunakan kontainer
PlusEmpty[F].monoid
paling luar, bukan kontainer bagian dalam. Sebagai contoh,
untuk List[Either[String, Int]]
, Left[String]
lah yang akan dikonversi ke
.empty
, bukan List[A]
, yang akan dikonversi menjadi .empty
. Dan dilanjutkan
dengan menggabungkan semuanya. Untuk pemrogram yang santai, metoda ini memberikan
kita kenyamanan untuk membuang galat galat yang mungkin terjadi.
withFilter
allows us to make use of for
comprehension language
support as discussed in Chapter 2. It is fair to say that the Scala
language has built-in language support for MonadPlus
, not just
Monad
!
withFilter
memperkenankan kita untuk menggunakna dukungan komprehensi for
yang sudah dibahas pada Bab 2. Hal ini menunjukkan bahwa Scala
sudah mendukung MonadPlus
dan tidak hanya Monad
saja.
Kembali ke Foldable
, kita akan menunjukkan beberapa metoda yang tidak
kita diskusikan sebelumnya
msuml
akan mem-fold
menggunakan Monoid
dari PlusEmpty[G]
dan
collapse
mem-foldRight
dengan menggunakan PlusEmpty
dari tipe target:
5.10 Penyendiri
Beberapa kelas tipe pada Scalaz tidak dapat menjadi bagian dari hierarki
seperti Monad
, Applicative
, Functor
dkk.
5.10.1 Zippy
Metoda inti dari kelas tipe ini adalah zip
yang bisa dianggap sebagai
Divide.tuple2
yang kurang fleksibel. Dan bila terdapat sebuah Functor[F]
maka zipWith
bisa berperilaku sebagaimana Apply.apply2
.
Dan menariknya, sebuah Apply[F]
dapat dibuat dengan mamnggil ap
dan
mengaplikasikannya pada Zip[F]
dan Functor[F]
.
apzip
, mirip dengan Functor.fproduct
menerima sebuah F[A]
dan sebuah
fungsi terangkat dari F[A] => F[B]
dan menghasilkan sebuah F[(A, B)]
.
Metoda utama dari kelas tipe Unzip
adalah unzip
dengan firsts
dan seconds
sebagai pemilih elemen pertama ataupun kedua dari pasangan
pada F
. Dan yang paling penting adalah, unzip
merupakan kebalikan
dari zip
.
Metoda unzip3
sampai unzip7
merupakan pengaplikasian yang diulang
dari unzip
untuk menghilangkan basa basi. Sebagai contoh, bila kita
menerima sebuah tuple berlapis, Unzip[Id]
bisa dengan sigap meratakannya:
Pendek kata, Zip
dan Unzip
merupakan versi yang lebih kaku dari
Divide
dan Apply
. Selain itu, kedua kelas tipe sebelumnya menyediakan
fitur fitur berguna tanpa harus menyaratkan penggunaan F
.
5.10.2 Optional
Pada dasarnya, Optional
adalah bentuk umum dari struktur data yang
mungkin mempunyai nilai, seperti Option
dan Either
.
Bila pembaca budiman ingat mengenai operator disjungsi (\/
), operator
tersebut merupakan perbaikan atas scala.Either
. Selain itu, Scalaz juga
memberikan operator lain sebagai peningkatan untuk scala.Option
.
Sebagaimana cuplikan diatas, tentu pembaca budiman cukup familiar
dengan metoda-metoda di atas. Satu metoda yang mungkin agak asing adalah
pextract
yang menerima sebuah Functor[A]
dan mengembalikan salah satu
dari F[B]
atau nilai a
. Sebagai contoh, Optional[Option].pextract
akan mengembalikan Option[Nothing] \/ A
.
Selain itu, Scalaz juga memberikan operator terner untuk apapun yang
mempunyai kelas tipe Optional
sebagai contoh
5.11 Co-
Tipe kelas dengan awalan “ko” pada umumnya, kelas ini merupakan lawan dari kelas tipe yang diawali “ko” tadi. Walaupun, bukan berarti kelas tipe ini selalu berupa invers. Untuk menunjukkan hubungan antara, misal, “sesuatu” dengan “ko-sesuatu”, kita akan mengikutsertakan penanda tipe dari “sesuatu” bilamana memungkinkan.
5.11.1 Cobind
cobind
(juga dikenal sebagai coflatmap
) menerima sebuah F[A] => B
dan beroperasi atas F[A]
, bukan A
. Walaupun hal ini bukan berarti
F[A]
harus benar benar merupakan fungtor dengan isi A
. Biasanya,
F[A]
yang dimaksud di sini merupakan sub-struktur yang didefinisikan
oleh cojoin
(atau coflatten
) yang mempunyai fungsi untuk memperluas
sebuah data struktur.
Contoh permasalahan yang cocok untuk diselesaikan oleh Cobind
sebenarnya
cukup sulit untuk ditemui. Walaupun, sebagaimana yang diperlihatkan pada
tabel permutasi Functor
, adalah sebuah hal yang sulit untuk menyanggah
mengenai penting atau tidaknya sebuah metoda bila dibandingka dengan metoda
lainnya:
metoda | parameter |
---|---|
map |
A => B |
contramap |
B => A |
xmap |
(A => B, B => A) |
ap |
F[A => B] |
bind |
A => F[B] |
cobind |
F[A] => B |
5.11.2 Comonad
.copoint
(atau .copure
) mengelupas sebuah elemen dari konteks yang
melingkupinya. Efek biasanya tidak mempunyai instans Comonad
karena
Comonad
akan menghapus transparansi rujukan saat penginterpretasian sebuah
IO[A]
menjadi A
. Namun, untuk struktur data koleksi, penggunaan .copoint
merupakan salah satu cara untuk mengakses semua elemen beserta elemen
yang berdekatan dengannya.
Misalkan, sebuah daerah sekeliling (disingkat Hood
dari neighbourhood) yang
terdiri atas sebuah senarai elemen bagian kiri (lefts
), elemen yang dilihat
(focus
), dan sebuah senarai elemen pada bagian kanan (rights
).
Struktur lefts
dan rights
harus dibuat dengan yang dimulai dari
focus
lalu semakin menjauh bila ditambahkan, sehingga kita bisa
mendapatkan kembali IList
awal dengan metoda .toIList
.
Kita dapat menulis metoda-metoda untuk memindah fokus ke kiri (previous
)
ataupun ke kanan (next
)
Dengan mengaplikasikan more
atas sebuah fungsi opsional secara berulang
kepada sebuah Hood
, kita dapat menghitung semua positions
yang
bisa digunakan pada Hood
yang bersangkutan
Sekarang, kita dapat mengimplementasikan Comonad[Hood]
cojoin
memberikan kita sebuah Hood[Hood[IList]]
yang berisi semua
Hood
ynag mungkin pada IList
awal kita
Dan memang, cojoin
sebenarnya adalah positions
! Tentu, kita dapat
meng-override
-nya dengan implementasi yang lebih lugas dan performan.
Comonad
menggeneralisasi konsep Hood
untuk semua struktur data.
Hood
merupakan sebuah contoh dari zipper (tidak ada hubungannya
dengan Zip
). Scalaz sendiri juga mempunyai sebuah tipe data Zipper
yang berhubungan dengan aliran (mis, struktur 1 dimensi tak hingga),
yang akan kita bahan pada bab selanjutnya.
Salah satu penggunaan dari sebuah zipper adalah automata seluler yang menghitung nilai tiap sel generasi selanjutnya dengan melakukan penghitungan terhadap sel-sel di sekeliling sel tadi.
5.11.3 Cozip
Walaupun dinamai sebagai cozip
, kelas tipe ini mungkin lebih cocok
bila dibicarakan sebagai simetri dari unzip
. Bilamana unzip
memisah
F[_]
dari tuple (produk) menjadi tuple F[_]
, cozip
memisah F[_]
dari disjungsi (ko-produk) menjadi disjungsi F[_]
.
5.12 Bi-
Seringkali kita menemui keadaan dimana kita mempunyai sebuah benda yang
mempunyai dua tipe, dan kita ingin memetakan keduanya ke kategori lain.
Sebagai contoh, mungkin kita ingin melacak galat pada bagian kiri sebuah
Either
dan ingin melakukan sesuatu pada pesan galat tersebut.
Kelas tipe Functor
/ Foldable
/ Traverse
mempunyai sepupu yang
janggal yang memperkenankan kita untuk memetakan dari satu kategori
ke kategori lainnya secarabolak balik.
Walaupun penanda tipe dari metoda-metoda di atas bisa
dikatakan sangat lantung, mereka tidak lain dan tidak bukan hanyalah
metoda inti dari Functor
, Foldable
, dan Bitraverse
yang menerima
dua fungsi, bukan satu. Selain itu, metoda-metoda tadi juga memaksa
kedua fungsi untuk mengembalikan tipe yang sama dengan pertimbangan
keluaran mereka dapat digabungkan menggunakan Monoid
ataupun Semigroup
.
Sebagai tamabahan, kita dapat meninjau kembali MonadPlus
(yang merupakan
Monad
dengan tambahan filterWith
dan unite
) dan mempertimbangkan
apakah kelas tipe ini bisa memisah konten Bifoldable
dari sebuah Monad
Hal ini sangat berguna bila kita mempunyai sebuah koleksi dari bi-things
dan kita ingin me-reorganisasi menjadi sebuah koleksi atas A
dan
koleksi atas B
5.13 Kesimpulan
Sesungguhnya, materi pada bab ini cukup banyak dan kita sudah mengeksplorasi pustaka standar untuk fungsionalitas polimorfis. Namun, bila kita harus membandingkan satu dengan lainnya, pustaka standar Koleksi milik Scala memiliki trait yang jauh lebih banyak bila dibandingkan dengan kelas tipe yang dimiliki oleh Scalaz.
Adalah hal yang jamak ditemui bila sebuah aplikasi pemrograman fungsional hanya menyentuh sebagian kecil dari hierarki kelas tipe. Hal itu juga dengan mempertimbangkan bahwa kebanyakan fungsionalitas berasal dari aljabar spesifik domain dan kelas tipe. Bahkan bila kelas tipe yang spesifik pada domain hanya merupakan salinan khusus dari kelas tipe Scalaz, kita bisa melakukan refaktor di lain waktu.
Sebagai tambahan, kita juga sudah mengikutsertakan contekan untuk kelas tipe dan metoda utamanya pada Lampiran. Contekan ini mendapatkan inspirasi dari Contekan Scalaz yang ditulis oleh Adam Rosien.
Lebih lanjut, Valentin Kasas menjelaskan mengenai Penggabungan N
thing:
6. Tipe Data Scalaz
Siapa yang tidak suka dengan struktur data keren? Tentu tidak ada, karena semua struktur data keren!
Pada bab ini, kita akan mengeksplorasi tipe data seperti koleksi yang ada pada Scalaz dan juga tipe data yang memperkaya Scala dengan semantik multi guna dan keamanan tipe data.
Alasan utama kita memberi perhatian lebih terhadap banyaknya jenis koleksi yang kita miliki adalah performa. Sebuah vektor dan senarai mampu melakukan hal yang sama, namun karakteristik performa mereka berbeda: sebuah vektor mempunyai beban pencarian konstan sendangkan senarai harus melangkahi elemen satu per satu.
Semua koleksi yang ditunjukkan di sini bersifat persisten: blia kita menambah ataupun menghapus sebuah elemen, kita masih bisa menggunakan koleksi sebelumnya. Pembagian struktural merupakan bagian penting bila kita berbicara mengenai performa dari struktur data persisten. Bila kita tidak memperhatikan pembagian struktural, koleksi akan dibuat ulang setiap kali operasi atas koleksi tersebut dilakukan.
Tidak seperti koleksi pada pustaka standar Java dan Scala, Scalaz tidak mempunyai hierarki tipe data: koleksi-koleksi ini lebih sederhana dan mudah dipahami. Fungsionalitas polimorfis tersedia dengan mengoptimisasi instans dari kelas tipe yang telah kita pelajari pada bab sebelumnya. Penggunaan instans kelas tipe sangat mempermudah kita dalam menukar implementasi dengan alasan performa ataupun dengan membuat implementasi kita sendiri.
6.1 Varian Tipe
Banyak dari tipe data Scalaz mempunyai parameter tipe yang bersifat invarian.
Sebagai contoh, IList[A]
bukan merupakan sub-tipe dari IList[B]
walau
A <: B
.
6.1.1 Kovarian
Salah satu permasalahan dari parameter tipe kovarian, seperti class
List[+A]
, adalah List[A]
juga merupakan sub-tipe dari List[Any]
.
Hal semacam ini sangat mempermudah hilangnya informasi tipe.
Harap perhatikan bahwa senarai kedua merupakan List[Char]
dan kompilator
menyimpulkan, walaupun ngaco, bahwa Batas Atas Terendah (BAT) sebagai
Any
. Bila dibandingkan dengan IList
, yang mengharuskan .widen[Any]
secara eksplisit untuk memberikan celah untuk kecerobohan semacam ini:
Hal yang sama juga terjadi ketika kompilator menyimpulkan bahwa
sebuah tipe with Product with Serializable
(yang berupa Product
dan Serializable), hal semacam ini merupakan indikator yang kuat
bahwa pelebaran tanpa sengaja telah terjadi
dikarenakan kovarian.
Sayangnya, kita harus berhati-hati saat menyusun tipe data invarian dikarenakan kalkulasi BAT dilakukan pada parameter:
Masalah yang mirip dengan hal ini juga terjadi pada tipe Nothing
milik Scala,
yang merupakan sub-tipe dari semua tipe, termasuk ADT sealed
, kelas final
,
primitif, dan null
.
Dikarenakan tidak ada nilai dari tipe Nothing
: fungsi yang menerima Nothing
sebagai salah satu parameter tidak dapat dijalankan dan fungsi yang mengembalikan
Nothing
tidak akan mengembalikan kembaliannya.
Nothing
pada awalnya diperkenalkan sebagai sebuah mekanisme untuk memperkenankan
kovarian pada parameter tipe. Walaupun, sebagai konsekuensinya yang tak disengaja,
kita juga bisa menghasilkan kode yang tak bisa dijalankan. Di sisi lain Scalaz
berpendapat bahwa kita tidak butuh parameter tipe kovarian. Hal ini berarti
bahwa kita membatasi diri kita untuk hanya menulis kode yang bisa dijalankan saja.
6.1.2 Kontrarivarian
Agak berbeda dengan kovarian, parameter tipe kontravarian, seperti
trait Thing[-A]
, bisa menimbulkan masalah tak terduga sebagaimana
yang ditunjukkan pada kutu di kompilator.
Paul Phillips (bekas anggota tim scalac
) juga telah mendemonstrasikan
apa yang dia sebut sebagai kontrari-varian.
Sebagaimana yang telah pembaca yang budiman terka, kompilator berhasil
menentukan argumen paling spesifik untuk setiap pemanggilan f
.
Namun, resolusi implisit dari kompilator memberikan hasil yang tak terduga:
Resolusi implisit membalik definisi kompilator atas “argumen paling spesifik” untuk tipe kontravarian sehingga argumen tersebut menjadi percuma bila digunakan dengan kelas tipe maupun semua yang menggunakan fungsionalitas polimorfis. Perilaku semacam ini sudah dibenahi pada Dotty.
6.1.3 Batasan dari Pembuatan Subtipe
Sebagaimana yang telah pembaca yang budiman ketahui, scala.Option
mempunyai metoda .flatten
yang akan mengubah Option[Option[B]]
menjadi
Option[B]
. Namun, sistem tipe Scala akan menggagalkan usaha kita
untuk menuliskan penanda tipe untuk metoda tersebut.
Mohon perhatikan pada contoh berikut yang terlihat benar anmun
mempunyai sebuah kutu yang hampir tak kasat mata:
A
yang diperkenalkan pada .flatten
membayangi A
yang diperkenalkan
pada kelas. Hal seperti ini sama saja dengan menuliskan
yang berbeda dengan batasan yang kita inginkan.
Untuk menyiasati batasan ini, Scala mendefinisikan kelas infiks <:<
dan =:=
beserta bukti implisit yang selalu meninggalkan sebuah saksi
=:=
bisa digunakan untuk memaksa kedua parameter tipe benar benar
sama. Sedangkan <:<
digunakan untuk mendeskripsikan hubungan sub-tipe.
Kedua kelas tersebut memperkenankan kita untuk mengimplementasikan
.flatten
sebagai
Scalaz memperbaiki kedua kelas tadi dengan menggunakan Liskov
(dialiaskan sebagai <~<
) dan Leibniz (===
).
Selain metoda-metoda umum yang tentu berguna dan konversi implisit,
bukti dari kelas <~<
dan ===
lebih memegang prinsip bila dibandingkan
dengan kelas <:<
dan =:=
milik pustaka standar.
6.2 Evaluasi
Pada bahasa pemrograman Java, evaluasi program dijalankan secara tegas:
semua parameter dari sebuah metoda harus dievaluasi menjadi sebuah nilai
sebelum metoda tersebut dipanggil. Scala, di sisi lain, memperkenalkan
istilah parameter by-name pada metoda dengan sintaks a: => A
.
Parameter ini dibungkus sebagai fungsi tanpa argumen yang dipanggil
tiap kali a
dirujuk. Seperti yang telah kita lihat pada bab-bab sebelumnya,
kelas tipe cenderung menggunakan parameter by-name.
Scala juga mempunyai strategi evaluasi nilai berdasarkan pemanggilan by-need,
menggunakan kata kunci lazy
: komputasi dilakukan paling banyak satu kali
ketika nilai parameter akan digunakan. Sayangnya, scala tidak mendukung
evaluasi komputasi dengan pemanggilan by-need pada parameter metoda.
Scalaz memformalisasi tiga strategi evaluasi yang menggunakan TDA
Bentuk evaluasi paling lemah adalah Name
yang tidak memberikan
jaminan komputasi. Selanjutnya adalah Need
, yang menjamin evaluasi
paling banyak satu kali. Dan evaluasi Value
yang merupakan nilai
hasil dari komputasi yang terjadi sebelum pemanggilan terjadi.
Evaluasi Value
menjamin satu kali evaluasi.
Bila kita berbengah diri, bisa saja kita munder ke kelas tipe dan membuat
metoda mereka untuk secara spesifik menerima parameter Name
, Need
,
atau Value
. Namun, kita memilih untuk mengasumsikan bahwa parameter
normal akan selalu dibungkus dalam sebuah Value
dan parameter by-name
dapat dibungkus dengan Name
.
Ketika kita menulis program murni, kita bebas untuk mengganti
Name
dengan Need
atau Value
, begitu juga sebaliknya, tanpa
mengubah kebenaran program. Yang menjadi esensi dari transparansi
rujukan adalah keluwesan untuk mengganti sebuah komputasi dengan nilai
komputasi tersebut atau mengganti nilai sebuah komputasi dengan komputasi
itu sendiri.
Pada pemrograman fungsional, kita hampir selalu menggunakan Value
atau
Need
(dikenal dengan tegas dan lundung) dikarenakan hampir tidak ada
untungnya menggunakan Name
secara eksplisit.
Hal ini dikarenakan tidak dukungan pada tingkat bahasa untuk parameter
metoda yang dipanggil secara lundung. Metoda secara umum meminta parameter
by-name lalu mengubahnya menjadi Need
secara internal agar mendapatkan
tambahan performa.
Name
menyediakan instans dari kelas tipe berikut:
Monad
Comonad
Traverse1
Align
-
Zip
/Unzip
/Cozip
6.3 Memoisasi
Scalaz mampu melakukan memoisasi fungsi yang belum pasti akan selalu
dievaluasi dikarenakan bermacamnya implementasi.
Secara formal, memoisasi diwakilkan dengan Memo
:
memo
memperkenankan kita untuk membuat implementasi khusus atas kelas
tipe Memo
. Sedangkan untuk nilMemo
, metoda ini tidak melakukan
memoisasi. Dengan kata lain, nilMemo
mengevaluasi fungsi secara normal.
Untuk implementasi metoda lainnya, mereka hanya mencegat pemanggilan
fungsi dan nilai yang tersimpan di tembolok dengan menggunakan
implementasi dari pustaka koleksi standar.
Untuk menggunakan Memo
, kita hanya perlu membungkus sebuah fungsi dengan
implmentasi Memo
dan dilanjutkan dengan memanggil fungsi ter-memoisasi
tadi:
Bila sebuah fungsi menerima lebih dari sebuah parametr, kita harus mengubah
parameter-parameter tadi menjadi sebuah tuple menggunakan metoda tupled
sehingga fungsi tadi berubah menjadi fungsi ter-memoisasi yang menerima
sebuah tuple.
Memo
pada dasarnya dianggap sebagai konstruk khusus dan penegakan
aturan mengenai kemurnian sedikit lebih longgar dengan
alasan memudahkan implmentasi. Agar tetap murni, yang perlu
kita lakukan hanyalah memastikan implementasi Memo
yang kita buat
untuk selalu secara melakukan transparansi saat merujuk pada saat evaluasi
K => V
. Kita bisa juga menggunakan data yang bisa bermutasi dan melakukan
I/O
pada implementasi Memo
, misal dengan LRU atau tembolok terdistribusi
tannpa harus mendeklarasikan efek pada penanda tipe. Bahasa pemrograman
fungsional lainnya punya mekanisme memoisasi terotomatis yang diatur oleh
lingkungan waktu jalan mereka. Memo
di sisi lain, merupakan satu-satunya
cara kita untuk menambal JVM agar mempunyai dukungan yang mirip.
6.4 Pelabelan
Pada bagian dimana kita memperkenalkan kelas tipe Monoid
, kita membuat sebuah
instans Monoid
untuk TradeTemplate
(yang disimbolkan dengan Monoid[TradeTemplate]
).
Namun, kita juga menemukan bahwa perilaku Scalaz tidak sesuai dengan ekspektasi
kita terhadap Monoid[Option[A]]
. Perbedaan perilaku semacam ini bukan sebuah
keluputan dari Scalaz: Seringkali kita akan mendapatkan tipe data yang bisa menerapkan
kelas tipe mendasar dengan banyak cara, namun tidak berperilaku sesuai dengan yang
kita inginkan.
Contoh sederhana atas permasalahan seperti ini adalah Monoid[Boolean]
(konjungsi &&
dan disjungsi ||
) dan Monoid[Int]
(perkalian dan penjumlahan).
Untuk menerapkan Monoid[TradeTemplate]
, kita terpaksa harus merusak harmonisasi
kelas tipe, atau tinggal menggunakan kelas tipe lain.
Untuk menyelesaikan masalah yang muncul pada penerapan beberapa kelas tipe pada
satu kelas, scalaz.Tag
bisa digunakan tanpa merusak koherensi dari kelas tipe
yang sudah ada.
Pendefinisian metoda Tag
memang agak rancu. Namun, sintaks yang digunakan
sangat jelas. Beginilah cara kita untuk mengelabuhi kompilator agar kita bisa mendefinisikan
tipe infiks A && T
yang menghapus penanda tipe menjadi A
pada saat waktu jalan:
Beberapa label yang bermanfaat yang disediakan pada objek Tags
First
/ Last
digunakan untuk memilih instans Monoid
dengan mengambil
oeran bukan-nol pertama / terakhir yang ditemui. Multiplication
, tentu,
digunakan untuk perkalian numerik, bukan penambahan. Disjunction
/ Conjunction
digunakan untuk memilih &&
atau ||
.
Pada TradeTemplate
, jauh lebih disukai untuk menggunakan Option[Currency] @@ Tags.Last
bila dibandingkan hanya menggunakan Option[Currency]
saja. Karena hal semacam ini
sangat jamak dijumpai, maka kita bisa menggunakan alias bawaan, LastOption
yang memperkenankan kita untuk menulis Monoid[TradeTemplate]
menjadi lebih jelas
Sedangkan bila kita harus membuat sebuah nilai mentah untuk tipe LastOption
,
kita bisa menggunakan Tag
pada sebuah Option
. Kita akan menyebut hal ini
sebagai Tag(None)
.
Pada bab mengenai derivasi kelas tipe, kita akan melangkah lebih lanjut dengan
melakukan derivasi otomatis atas monoid
.
Tentu sangat menggiurkan untuk menggunakan Tag
agar tipe data pada
validasi borang (mis, String @@ PersonName
), namun hal ini harus dihindari
karena tidak ada pemeriksaan konten pada saat waktu jalan. Tag
seharusnya
hanya boleh digunakan untuk pemilihan kelas tipe saja. Pembaca budiman
dianjurkan untuk menggunakan pustaka Refined
yang diperkenalkan pada bab
4 untuk membatasi nilai.
6.5 Transformasi Natural
Pada Scala, penulisan sebuah fungsi yang memetakan sebuah tipe ke tipe lainnya
biasa dituliskan sebagai A => B
. Penulisan tersebut sendiri merupakan
pemanis sintaksis untuk Function[A, B]
. Sedangkan untuk memetakan konstruktor
tipe F[_]
ke G[_]
. Scalaz menyediakan pemanis sintaks yeang mirip dengan
A => B
yaitu F ~> G
.
F ~> G
disebut sebagai transformasi natural dan secara umum terkuantifikasi
karena sintaks ini tidak menghiraukan isi dari F_]
.
Sebagai contoh transformasi natural, mari kita lihat sebuah fungsi yang
mengubah IList
menjadi List
atau yang lebih ringkas, dengan menggunakan pemanis kind-projector
:
Namun pada tahap pengembangan sehari-hari, sangat mungkin kita menggunakan
transformasi natural untuk memetakan dari aljabar satu ke aljabar lainnya.
Sebagai contoh, pada drone-dynamic-agents
, kita mungkin lebih memilih
untuk mengimplementasikannya dengan menggunakan aljabar yang sudah ada,
BigMachines
. Setelah mengetahui adanya transformasi ini, kita mungkin
akan memilih untuk melakukan transformasi dengan menggunakan Machine ~> BigMachines
daripada secara manual menulis ulang logika bisnis dan test kita menggunankan
BigMachine
. Kita akan kembali membahas gagasan ini pada bab mengenai
Monad Lanjutan.
6.6 Isomorphism
Seringkali kita mendapati dua tipe yang benar-benar sama dan mengakibatkan masalah kompatibilitas yang dikarenakan kompilator tidak mengetauhi asumsi- asumsi yang kita ketahui. Hal ini biasanya terjadi bila kita menggunakan kode dari pihak ketiga yang sama dengan kode kita yang sudah ada.
Masalah seperti ini bisa diselesaikan dengan Isomorphism
. Sebuah isomorfisme
mendefinisikan secara formal hubungan setara antara dua tipe. Isomorphism
mempunyai tiga varian berdasarkan perbedaan bentuk dari tipe:
Tipe alias IsoSet
, IsoFunctor
, dan IsoBifunctor
mencakup hal-hal
umum: fungsi reguler, transformasi atural, dan transformasi binatural.
Fungsi fungsi pembantu mempermudah kita dalam membuat instans dari fungsi
fungsi atau transformasi natural yang telah ada sebelumnya. Walaupun
kadangkala, akan lebih mudah dalam pendefinisian isomorfisme dengan
menggunakan abstrak Template
. Sebagai contoh:
Bila kita memperkenalkan sebuah isomorfisme, kita juga akan membuat banyak instans kelas tipe standar. Sebagai contoh:
memperkenankan kita untuk menderivasi sebuah Semigroup[F]
untuk tipe F
bila
kita mempunyai sebuah F <=> G
dan Semigroup[G]
. Hampir semua kelas tipe
pada hierarki menyediakan varian isomorfik. Bila kita berada pada situasi
salin-tempel saat menulis implementasi kelas tipe, mungkin ada baiknya
mempertimbangkan Isomorphism
sebagai solusi yng lebih baik.
6.7 Kontainer
6.7.1 Maybe
Sebagaimana yang telah kita saksikan, Scalaz menyediakan peningkatan atas
scala.Option
dengan konstruk Maybe
. Maybe
dianggap sebagai peningkatan
dikarenakann konstruk ini merupakan sebuah invarian dan tidak mempunyai
metoda rawan seperti Option.get
, yang bisa melempar pengecualian.
Secara umum, konstruk ini digunakan untuk merepresentasikan keadaan dimana sebuah objek bisa ada maupun tidak, tnapa memberikan konteks kenapa bisa begitu.
Metoda pasangan .empty
dan just
lebih disukai saat membuat instans
Empty
dan Just
mentah karena kedua metoda tersebut mengembalikan
sebuha Maybe
dan membantu mempermudah pendugaan tipe. Pola ini seringkali
digunakan karena mengembalikan a sum type. Sum type sendiri
merupakan keadaan dimana kita mempunyai beberapa implementasi sebuah
sealed trait
namun tidak menggunakan sub-tipe khusus pada sebuah penanda tipe.
Kita juga bisa tinggal memanggil .just
pada semua nilai dan mendapatkan sebuah
Maybe
. Hal ini dikarenakan kelas pembantu implicit class
Maybe
mempunyai instans kelas tipe untuk
Align
Traverse
-
MonadPlus
/IsEmpty
Cobind
-
Cozip
/Zip
/Unzip
Optional
dan mendelegasi instans yang bergantung pada A
-
Monoid
/Band
-
Equal
/Order
/Show
Sebagai tambahan untuk kelas tipe di atas, Maybe
juga mempunyai
beberapa fungsionalitas yang tidak didukung oleh kelas tipe polimorfis.
.cata
merupakan bentuk singkat dari .map(f).getOrElse(b)
dan bahkan
mempunyai bentuk yang lebih sederhana dalam bentuk |
bila map berupa
sebuah identity
(mis, hanya .getOrElse
).
.toLeft
dan toRight
, dan alias simbolis mereka, membuat sebuah disjungsi
(yang akan dijelaskan pada bagian selanjutnya) dengan menerima sebuah
penadah untuk kasus Empty
.
.orZero
menerima sebuah Monoid
untuk mendefinisikan nilai bawaan.
orEmpty
menggunakan ApplicativePlus
untuk membuat sebuah elemen atau
kontainer kosong, tanpa melupakan bahwa kita sudah mempunyai dukungan
untuk pustaka koleksi standar dari metoda .to
dari instans Foldable
.
6.7.2 Either
Untuk perbaikan yang diberikan oleh Scalaz terhadap scala.Either
,
walaupun hanya dalam bentuk simbol operator, adalah hal yang jamak untuk
menyebut operator tersebut sebagai antara (either)atau Disjunction
.
dengan sintaks
Harap diperhatikan, metoda ekstensi di atas menerima tipe untuk
sisi yang berseberangan. Jadi, bila kita ingin membuat sebuah
String \/ Int
dan kita mempunyai sebuah Int
, kita harus menyerahkan
String
saat memanggil .right
Sifat simbolis dari \/
-lah yang mempermudah pembacaan kontainer ini
pada penanda tipe. Harap diperhatikan bahwa tipe simbolis pada Scala
selalu diasosiasikan dari kiri. Ditambah lagi bila kita ingin menggunakan
\/
berlapis, kita harus menggunakan tanda kurung. Sebagai contoh,
(A \/ (B \/ (C \/ D)))
.
\/
mempunyai kecenderungan untuk memilih bagian kanan (mis, flatMap
juga berlaku pada \/-
) untuk instans kelas tipe:
-
Monad
/MonadError
-
Traverse
/Bitraverse
Plus
Optional
Cozip
dan bergantung pada konten
-
Equal
/Order
-
Semigroup
/Monoid
/Band
Sebagai tambahan, ada beberapa metoda khususs
.fold
mirip dengan Maybe.cata
dan mengharuskan kedua sisi dipetakan
ke tipe yang sama.
.swap
menukar sisi kiri ke kanan dan sebaliknya.
|
yang merupakan alias dari getOrElse
terlihat mirip dengan Maybe
.
Kita juga bisa menggunakan |||
sebagai alias untuk orElse
.
+++
merupakan penggabungan disjungsi dengan kecenderungan untuk memilih
bagian kiri:
-
right(v1) +++ right(v2)
menghasilkanright(v1 |+| v2)
-
right(v1) +++ left (v2)
menghasilkanleft (v2)
-
left (v1) +++ right(v2)
menghasilkanleft (v1)
-
left (v1) +++ left (v2)
menghasilkanleft (v1 |+| v2)
.toEither
disediakan untuk kompatibilitas terbalik dengan pustaka
standar Scala.
Untuk kombinasi dari :?>>
dan <<?:
memperkenankan kita untuk
menghiraukan isi dari sebuah \/
, namun berdasarkan tipe dari isinya.
6.7.3 Validation
Secara sekilas, Validation
yang mempunyai alias dengan \?/,
terlihat seperti salinan dari
Disjunction`:
Dengan sintaks
Namun, struktur data tersebut tidak mewakili cerita yang melatar-
belakanginya. Validation
memang dimaksudkan untuk tidak memiliki
instans dari Monad
dan membatasi dirinya berdasarkan versi
yang diharapkan dari:
Applicative
-
Traverse
/Bitraverse
Cozip
Plus
Optional
dan berdasarkan konten
-
Equal
/Order
Show
-
Semigroup
/Monoid
Keuntungan utama atas pembatasann yang hanya sampai pada Applicative
adalah pada saat kita membutuhkan semua galat dilaporkan, Validation
akan menerima semua galat tersebut. Berbeda dengan Disjunction
yang
berhenti dieksekusi pada saat galat pertama terjadi. Untuk mengakomodasi
akumulusai galat, bentuk paling umum yang ditemui dari Validation
adalah
ValidationNel
yang mempunyai NonEmptyList[E]
pada posisi galat.
Misalkan saat pembaca yang budiman sedang melakukan validasi terhadap
data yang diberikan oleh pengguna menggunakan Disjunction
dan flatMap
:
Bila kita menggunakan |@|
Kita akan tetap mendapat galat pertama saja. Hal ini disebabkan oleh
kelas tipe dari Disjunction
yang juga mempunyai instans Monad
.
Metoda .applyX
harus konsisten dengan .flatMap
dan tidak mengasumsikan
bahwa semua operasi bisa dijalankan secara bebas. Bandingkan dengan:
Sekarang, kita bakal mendapat semua galat yang terjadi.
Validation
punya beberapa metoda yang mirip dengan yang dipunyai
oleh Disjunction
seperti, .fold
, .swap
, +++
, dan beberapa tambahan:
.append
(dengan alias +|+
) mempunyai penanda tipe yang sama dengan +++
namun lebih memilih hasil yang success
-
failure(v1) +|+ failure(v2)
menghasilkanfailure(v1 |+| v2)
-
failure(v1) +|+ success(v2)
menghasilkansuccess(v2)
-
success(v1) +|+ failure(v2)
menghasilkansuccess(v1)
-
success(v1) +|+ success(v2)
menghasilkansuccess(v1 |+| v2)
.disjunction
mengubah sebuah Validated[A, B]
menjadi A \/ B
.
Disjunction
mencerminkan .validation
dan .validationNel
dan
mengubahnya menjadi Validation
. Sehingga hal ini mempermudah
konversi dari akumulasi galat berurutan dan paralel.
\/
dan Validation
merupakan solusi dari pemrograman fungsional yang
setara dengan pemeriksaan pengecualian untuk validasi input.
Selain itu, performa yang ditawarkan lebih tinggi dikarenakan Validation
tidak menggunakan stacktrace dan tanpa memaksa metoda pemanggil untuk
berurusan dengan galat. Hal semacam ini menghasilkan sistem yang lebih
kokoh.
6.7.4 These
Seperti yang telah kita temui pada bab sebelumnya mengenai Align
,
These
berbicara mengenai penyandian data dengan logika inklusif atau
yang disebut juga OR
.
dengan sintaks konstruktor
These
mempunyai instans kelas tipe untuk
Monad
Bitraverse
Traverse
Cobind
dan bergantung dengan konten
-
Semigroup
/Monoid
/Band
-
Equal
/Order
Show
These
(\&/
) mempunyai banyak metoda yang setara dengan metoda
dari Disjunction
(\/
) dan Validation
(\?/
)
.append
mempunyai 9 cara penyusunan yang mungkin dibuat dan data tidak
pernah dibuang dikarenakan This
dan That
selalu bisa dikonversi menjadi
Both
.
.flatMap
merupakan metoda yang cenderung memilih parameter sebelah kanan.
.flatMap
menerima sebuah Semigroup
pada konten bagian kiri (This
)
untuk digabungkan, bukan meng-arus-pendekkannya. &&&
dapat digunakan
untuk menggabungkan dua These
dan membuat sebuah tuple di bagian kanan
dan memmbuang data yang bersangkutan bila data tersebut tidak ada pada
kedua sisi These
.
Walaupun merupakan hal yang menggiurkan untuk menggunakan \&/
pada
tipe kembalian, penggunaan berlebihan merupakan salah satu anti-pattern.
Alasan utama untuk menggunakan \&/
adalah untuk menggabungkan atau
memecah aliran data yang bisa jadi tak hingga pada memori yang hingga.
Fungsi pembantu ada pada objek pendamping bila dibutuhkan bila berurusan
dengan EphemeralStream
atau apapun dengan sebuah MonadPlus
.
6.7.5 Higher Kinded Either
Tipe data Coproduct
(berbeda dengan konsep umum ko-produk pada sebuah ADT)
membungkus Disjunction
untuk konstruktor tipe:
Instans kelas tipe diserahkan ke fungtor F[_]
dan G[_]
.
Penggunaan Coproduct
yang paling jamak dijumpai adalah saat kita ingin
membuat sebuah ko-produk anonimus untuk sebuah GADT.
6.7.6 Jangan Terburu-Buru
Tipe data tuple bawaan dari pustaka standar Scala dan tipe data sederhana
seperti Maybe
dan Disjunction
merupakan tipe dengan nilai yang selalu
dievaluasi secara tegas.
Untuk memudahkan pemakaian, alternatif by-name untuk Name
juga disediakan
beserta beberapa instans kelas tipe:
Pembaca yang teliti akan memperhatikan bahwa Lazy*
merupakan salah kaprah
dan tipe data ini seharusnya ByNameTupleX
, ByNameOption
, and ByNameEither
.
6.7.7 Const
Const
, untuk konstan, merupakan pelapis untuk nilai dari tipe A
, beserta
sebuah tipe parameter cadangan B
.
Const
menyediakan sebuah instans dari Applicative[Const[A, ?]]
bila
Monoid[A]
tersedia:
Yang menjadi esensi dari Applicative
ini adalah Applicative
tersebut
menghiraukan parameter B
dan melanjutkan eksekusi dengan lancar dan
hanya mengkombinasikan nilai konstan yang ditemu.
Kembali ke contoh aplikasi drone-dynamic-agents
, kita harus me-refaktor
berkas logic.scala
terlebih dahulu agar menggunakan Applicative
.
Sebelumnya, kita menggunakan Monad
karena kita tidak tahu ada alternatif
yang lebih sesuai untuk konteks permasalahan ini.
Karena logika bisnis kita hanya membutuhkan sebuah Applicative
, kita bisa
menulis tiruan implementasi F[a]
dengan Const[String, a]
. Pada setiap
kasus, kita mengembalikan nama dari fungsi yang dipanggil.
Dengan interpretasi program kita semacam ini, kita dapat memastikan pada metoda-metoda yang ada bahwa ada
Bisa juga kita menghitung jumlah pemanggilan metoda secara keseluruhan
dengan menggunakan Const[Int, ?]
atau IMap[String, Int]
.
Dengan tes semacam ini, kita sudah jauh melampau tes tiruan tradisional
dengan menggunakan test Const
yang memeriksa apa yang dites tanpa harus
menyediakan implementasi. Hal semacam ini berguna bila spesifikasi kita
mengharuskan untuk menerima input untuk panggilan-panggilan tertentu.
Terlebih lagi, kita mencapai hasil ini dengan keamanan waktu kompilasi.
Melanjutkan penggunaan pola pikir semacam ini sedikit lebih jauh, misal
kita ingin memonitor node yang kita hentikan pada act
. Kita bisa
membuat implementasi Drone
dan Machines
dengan Const
dan memanggilnya
dari metoda act
Kita bisa melakukan hal semacam ini karena monitor
merupakan metoda
murni yang berjalan tanpa menghasilkan efek samping.
Potongan kode ini menjalankan program dengan ConstImpl
yang dilanjutkan
dengan mengekstrak semua pemanggilan ke Machines.stop
dan pada akhirnya
mengembalikan bersama WorldView
. Kita bisa mengetesnya dengan:
Kita sudah menggunakan Const
untuk melakukan apa yang terlihat seperti
Pemrograman Berorientasi Aspek, yang dulu pernah populer di Java. Kita
membangun logika bisnis kita untuk mendukung pemantauan tanpa harus
mengaburkan logika bisnis.
Dan menariknya, kita dapat menjalankan ConstImpl
pada lingkungan produksi
untuk mengumpulkan apa yang ingin kita stop
dan menyediakan implementasi
teroptimis dari act
yang bisa menggunakan kelompok panggilan implementasi
khusus.
Namun, pahlawan tanpa tanda jasa dari cerita ini adalah Applicative
.
Const
memperkenankan kita untuk menunjukkan apa yang bisa kita lakukan.
Bila kita harus mengubah program kita untuk meminta sebuah Monad
, kita
tidak dapat lagi menggunakan Const
dan harus menulis ulang tiruan secara
menyeluruh untuk dapat memastikan apa yang dipanggil pada input tertentu.
Rule of Least Power memaksa kita untuk memilih menggunakan Applicative
dibandingkan Monad
bila memungkinkan.
6.8 Koleksi
Berbeda halnya dengan APA Koleksi dari pustaka standar, pendekatan Scalaz
atas perilaku koleksi dideskripsikan dengan hierarki kelas tipe, misalkan
Foldable
, Traverse
, Monoid
. Yang tersisa untuk dipelajari adalah
implementasi strukutur data yang mempunyai karakteristik performa yang
cukup berbeda dan metoda relung.
Bagian ini langsung berbicara mengenai detail implementais untuk tiap tipe data. Tidak perlu mengingat semua yang ditunjukkan disini: tujuan utamanya adalah memahami konsep umum atas cara kerja tiap struktur data.
Karena semua tipe data koleksi menyediakan instans kelas tipe yang kurang lebih sama, kita akan melewatkan senarai instans tersebut, yang biasanya terdiri atas variasi dari:
Monoid
-
Traverse
/Foldable
-
MonadPlus
/IsEmpty
-
Cobind
/Comonad
-
Zip
/Unzip
Align
-
Equal
/Order
Show
Struktur data yang sudah terbukti tidak kosong akan menyediakan
-
Traverse1
/Foldable1
Dan menyediakan Semigroup
, bukan Monooid
, dan Plus
, bukan IsEmpty
.
6.8.1 Senarai
Kita sudah menggunakan IList[A]
dan NonEmptyList[A]
berulang kali
sehingga akan terasa familiar. Kedua struktur data tersebut mengkodifikasikan
struktur data klasik senarai berantai:
Keuntungan utama dari IList
atas List
milik pustaka standar adalah
tidak adanya metoda yang tidak aman seperti .head
yang melempar eksepsi
pada sebuah senarai kosong.
Sebagai tambahan, IList
jauh lebih sederhana, tanpa hierarki dan mempunyai
jejak bytecode yang jauh lebih kecil. Terlebih lagi, List
pustaka standar
mempunayi implementasi yang mengerikan dengan menggunakan var
untuk
mengakali masalah performa pada desain koleksi pustaka standar:
Pembuatan List
membutuhkan sinkronisasi Thread
yang hati hati dan pelan
untuk memastikan keamanan. IList
tidak membutuhkan tambalan
semacam itu sehingga mempunyai performa yang lebih bagus bila dibandingkan
List
.
6.8.2 EphemeralStream
Struktur data Stream
dari pustaka standar merupakan bentuk lundung
dari List
, namun implementasinya dipenuhi dengan kebocoran memori dan metoda
tak aman. Untuk menghilangkan masalah semacam ini, EphemerealStream
tidak
menyimpan rujukan pada nilai yang telah dikomputasi. Selain itu, sama halnya
dengan apa yang dilakukan pada IList
, EphemerealStream
juga menghilangkan
penggunaan metoda-metoda tidak aman.
.cons
, .unfold
, dan .iterate
digunakan untuk membuat stream. Sedangkan
untuk ##::
, operator ini digunakan untuk menambah elemen baru pada bagian
awal dari Estream
. Untuk .unfold
, seringkali digunakan untuk membuat
stream hingga (walaupun bisa saja merupakan stream tak hingga) dengan
mengaplikasikan fungsi f
secara berulang untuk mendapatkan nilai selanjutnya
dan input untuk fungsi f
itu sendiri. .iterate
membuat sebuah stream
tak hingga dengan mengulang fungsi f
pada elemen sebelumnya.
Estream
bisa saja muncul pada pattern match dengan simobl ##::
dengan
mencocokkan sintaks untuk .cons
.
Walau Estream
menjawab masalah memori, struktur data ini bisa saja
tetap terkena masalah memori bila ada nilai yang masih dirujuk terletak
pada bagian ujung awal dari sebuah stream tak hingga. Masalah semacam ini,
sebagaimana halnya dengan kebutuhan untuk membangun stream dengan efek,
merupakan alasan dibuatnya fs2.
6.8.3 CorecursiveList
Korekursi adalah saat kita memulai sesuatu dari sebuah kondisi awal dan
membuat langkah-langkah selanjutnya secara deterministik yang sama halnya
dengan EphemerealStream.unfold
yang baru saja kita pelajari.
Sangat berbeda dengan rekursi, yang memecah data menjadi kondisi dasar lalu berakhir.
CorecursiveList
merupakan penyandian data dari EphemerealStream.unfold
yang memberikan alternatif untuk EStream
yang berpeluang untuk memberikan
performa yang lebih bagus dalam beberapa situasi tertentu:
Korekursi berguna saat mengimplementasikan Comonad.cojoin
, seperti contoh
pada Hood
. CorecursiveList
merupakan contoh untuk mengkodifikasi persamaan
non-linear berulang seperti yang digunakan pada pemodelan biologi populasi,
sistem kontrol, ekonomi makro, dan investasi perbankan.
6.8.4 ImmutableArray
Sebuah pembungkus sederhana untuk struktur data Array
dengan spesialisasi
primitif:
Bila kita berbicara mengenai performa pembacaan dan ukuran heap, tidak
ada yang mengalahkan Array
. Namun, pembagian struktural sama sekali
tidak ada saat pembuatan array baru. Tiadanya penggunaan struktur memori
yang sama seperti ini merupakan salah satu alasan untuk menggunakan deret
(array) untuk data yang tidak diharapkan untuk berubah.
6.8.5 Dequeue
Dequeue
, diucapkan seperti “dek” kapal, merupakan senarai berantai
yang memperkenankan penambahan dan pengambilan item dari depan maupun
dari belakang dengan waktu konstan. Penghapusan elemen dari ujung-ujungnya
juga menggunakan waktu konstan.
Cara kerja dari Dequeue
adalah dengan menggunakan dua daftar, satu
di depan dan lainnya di belakang. Anggap sebuah instans yang berisi
simbol a0, a1, a2, a3, a4, a5, a6
yang dapat digambarkan sebagai
Harap perhatikan bahwa senarai pada back
disusun secara terbalik.
Untuk membaca snoc
(elemen paling akhir) hanya merupakan pencarian
sederhana pada back.head
. Sedangkan untuk penambahan sebuah elemen
pada akhir Dequeue
dilakukan dengan menambahkan sebuah elemen pada
bagian awal dari back
dan membuat ulang kulit FullDequeue
(yang
akan menambah ukuran backSize
). Hampir semua struktur data awal
akan digunakan ulang bila terjadi perubahan. Sebagai perbandingan,
penambahan sebuah elemen pada ujung belakan IList
akan menciptakan
seluruh struktur yang baru.
frontSize
dan backSize
digunakan untuk menyeimbangkan ulang front
dan back
sehingga ukuran keduanya kurang lebih sama. Penyeimbangan ulang
juga berarti bahwa beberapa operasi akan lebih lamban bila dibandingkan
dengan operasi lainnya (mis, saat pembangunan ulang struktur secara
menyeluruh). Namun, hal ini hanya kadang terjadi. Untuk penyederhanaan,
kita bisa mengambil rerata dari waktu penggunaan dan menganggapnya
konstan.
6.8.6 DList
Senarai berantai mempunyai karakteristik performa yang kurang baik bila senarai berukuran besar digabungkan. Sebagai gambaran, silakan diperhatikan operasi yang berjalan saat mengevaluasi potongan kode berikut:
Operasi tersebut membuat enam senarai sementara, melangkahi, dan membangun
ulang tiap senarai sebanyak tiga kali (kecuali gs
yang dibagi pada
semua tahap).
DList
(difference list) merupakan solusi yang lebih efisien untuk
skenario semacam ini. Kita tidak melakukan evaluasi pada tiap tahap,
namun kita merepresentasikannya sebagai sebuah fungsi IList[A] => IList[A]
Kalkulasi yang ekuivalen adalah (simbol dibuat dengan menggunakan
DList.fromIList
)
yang membagi tugas menjadi penambahan dengan sifat asosiatif-kanan
menggunakan konstruktor pada IList
.
Sebagaimana biasanya, selalu ada harga yang harus dibayar. Terdapat
alokasi memori tambahan yang dapat memperlambat kode yang berasal dari
penambahan yang bersifat asosiatif-kanan. Operasi yang mendapatkan
percepatan paling besar adalah ketika operasi pada IList
bersifat
asosiatif kiri, mis,
Bila DList
bernama ListBuilderFactory
, sangat memungkinkan
bahwa struktur data ini akan ada pada pustaka standar. Namun
karena reputasi yang buruk, hal ini tidak terjadi.
6.8.7 ISet
Struktur pohon dikenal sangat cocok untuk menyimpan data yang terurut dengan tiap simpul berisi elemen bernilai yang lebih kecil dari pada satu cabang dan lebih besar bila dibandingkan pada cabang lainnya. Namun implementasi naif atas struktur data pohon dapat menyebabkan tidak seimbangnya pohon tersebut pada saat penyisipan elemen. Juga memungkinkan untuk memiliki pohon yang seimbang namun sangat tidak efisien dilakukan karena tiap kali penyisipan elemen dilakukan, pohon tersebut akan dibangun ulang.
ISet
merupakan implementasi dari pohon dengan keseimbangan berbatas yang
berarti pohon ini diperkirakan seimbang, dengan menggunakan ukuran (size
)
dari tiap cabang untuk menyeimbangka sebuah simpul.
ISet
mengharap A
untuk mempunyai kelas tipe Order
. Instans Order[A]
harus tetap sama disela tiap pemanggilan. Bila tidak, asumsi internal akan
invalid dan menyebabkan korupsi data: mis, kita mengasumsikan koherensi
kelas tipe dimana Order[A]
unik untuk tiap A
.
Sayangnya, ADT ISet
melarang adanya pohon invalid. Kita akan berusaha untuk
menulis ADT yang mendeskripsikan secara lengkap mengenai apa yang valid dan
tidak dengan menggunakan pembatasan tipe. Namun, kadang kala ada beberapa
situasi yang menyebabkan hal ini hanya dapat dicapai saat mendapat bisikan
dari leluhur. Tip
/ Bin
dibuat private
untuk mencegah pengguna tanpa
sadar membuat pohon yang invalid. .insert
merupakan satu-satunya cara
untuk membuat ISet
. Sehingga, .insert
merupakan pendefinisian dari
sebuah pohon yang valid.
Metoda internal .balanceL
dan .balanceR
merupakan pencerminan satu sama
lain. Sehingga, kita hanya perlu mempelajari .balanceL
yang akan dipanggil
ketika nilai yang kita sisipkan kurang dari nilai yang ada pada simpul saat ini.
Metoda ini juga dipanggil oleh metoda .delete
.
Menyeimbangkan sebuah pohon mengharuskan kita untuk mengklasifikasi
skenario yang mungkin terjadi. Kita akan melihat satu persatu dan memvisualisasi
(y, left, right)
yang ada pada bagian kira laman dan struktur yang sudah
diseimbangkan pada bagian kanan. Hal ini juga dikenal sebagai pohon yang dirotasi.
- lingkaran yang terisi melambangkan sebuah
Tip
- tiga kolom melambangkan nilai
left | value | right
dariBin
- wajik melambangkan
ISet
Skenario pertama merupakan contoh sepele, dimana kedua sisi merupakan Tip
.
Nyatanya, kita tidak akan pernah menemui hal semacam ini dari pemanggilan
.insert
. Namun, kita akan menemukannya dengan pemanggilan .delete
Pada contoh kedua, left
merupakan sebuah Bin
yang hanya berisi sebuah
Tip
. Kita tidak perlu menyeimbangkan apaun, cukup membuat kesimpulan
sederhana:
Contoh ketiga-lah yang menarik: left
berupa sebuah Bin
yang merisi Bin
pada right
Apa yang terjadi pada kedua wajik yang ada pada di bawah lrx
?
Apakah kita akan kehilangan informasi? Tentu tidak, kita tidak kehilangan
informasi karena kita dapat menalar (menggunakan penyeimbangan ukuran) bahwa
kedua wajik tersebut menjadi Tip
.
Tidak ada aturan khusus untuk skenario berikut (atau pada .balanceR
) yang
dapat membuat sebuah pohon dimana sebuah wajik-lah yang menjadi Bin
.
Contoh keempat merupakan kebalikan dari contoh ketiga.
Untuk contoh ke lima, kita mempunyai pohon yang lengkap pada kedua sisi
dari left
dan kita harus menggunakan ukuran relatifnya untuk menentukan
bagaimana kita harus menyeimbangkan.
Pada cabang pertama, 2ll.size > lr.size
dan untuk cabang kedua 2ll.size <= lr.size
Pada skenario ke enam, kita akan mendapatkan sebuah pohon pada right
.
Saat left
kosong, kita menarik sebuah sambungan sederhana. Skenario ini
tidak pernah muncul dari .insert
karena left
tidak boleh kosong:
Skenario akhir adalah kondisi dimana kita tidak mempunyai pohon yang tidak kosong
pada kedua sisinya. Bila left
tidak lebih dari tiga kali ukuran dari right
kita hanya tinggal membuat sebuah Bin
Namun, bila left
berukuran tiga kali ataupun lebih bila dibandingkan right
,
kita harus menyeimbangkan pohon tersebut dahulu berdasarkan ukuran dari
ll
dan lr
seperti pada skenario ke lima.
Skenario ini menutup pembelajaran kita atas metoda .insert
dan bagaimana
ISet
dibangun. Seharusnya bukan hal yang mengejutkan bila Foldable
diimplementasikan dalam bentuk pencarian pertama mendalam pada left
dan right
.
Metoda semacam .minimum
dan .maximum
akan optimum diimplementasikan karena
struktur data sudah tersandikan berurutan.
Hal yang patut diperhatikan adalah beberapa metoda pada kelas tipe tidak dapat
diterapkan secara efisien. Misal, penanda untuk Foldable.element
Penerapan yang paling jelas untuk .element
adalah dengan menunda pencarian
biner ISet.contains
. Walaupun demikian, hal ini tidak mungkin dilakukan karena
.element
menyediakan Equal
, sedangkan .contains
meminta Order
.
Karena beberapa hal, ISet
tidak dapat menyediakan Functor
. Di lapangan,
ternyata hal ini menjadi batasan yang masuk akal: melakukan pemetaan .map
juga berarti membangun ulang struktur secara keseluruhan. Tentu hal yang
masuk akal untuk mengkonversi tipe data lain, seperti IList
, dilanjutkan
dengan melakukan pemetaan .map
, dan diakhiri dengan konversi ulang.
Sebuah konsekuensi yang muncul adalah kita tidak mungkin mempunyai Traverse[ISet]
maupun Applicative[ISet]
.
6.8.8 IMap
Terlihat familiar, bukan? Dan memang demikian adanya. IMap
yang mempunyai
alias ==>>
, merupakan pohon dengan ukuran yang diseimbangkan dan ditambah
dengan sebuah bidang tambahan value: B
pada tiap cabang biner.
Tambahan ini memperkenankan pohon data ini untuk menyimpan pasangan kunci/nilai.
Batasan untuk kunci/nilai pada pohon ini hanyalah tipe kunci A
harus mempunyai
instans Order
. Selain itu, ada beberapa metoda tersedia yang dapat digunakan
untuk memutakhirkan isi dari pohon ini.
6.8.9 StrictTree
dan Tree
StrictTree
dan Tree
merupakan penerapan dari pohon beringin.
Pohon beringin sendiri merupakan struktur pohon dengan jumlah cabang yang tak
dibatasi pada tiap simpulnya. Kedua struktur data ini, dibangun dengan
menggunakan pustaka koleksi dari pustaka standar dikarenakan alasan
peninggalan masa lalu:
Tree
merupakan versi by-need dari StrictTree
dengan konstruktor
Secara umum, pengguna pohon beringin diharapkan untuk menyeimbangkan pohon ini secara manual. Dengan demikian, struktur ini cocok untuk digunakan pada domain tertentu untuk menyandikan hierarki pada struktur data. Sebagai contoh, pada kecerdasan buatan, sebuah pohon beringin dapat digunakan pada algoritma pengelompokan untuk mengelompokkan data menjadi sebuah hierarki atas hal hal yang semakin mirip. Struktur ini juga bisa digunakan untuk merepresentasikan dokumen XML.
Saat bekerja dengan struktur data hierarkis, adalah cukup bijak untuk mempertimbangkan untuk menggunakan struktur data ini, bukan membuat struktur data sendiri.
6.8.10 FingerTree
Finger Tree (selanjutnya disebut pohon palem) merupakan deretan yang digeneralisasikan
dengan beban pencarian konstan yang teramortisasi dan penggabungan logaritmik.
A
merupakan tipe data dan untuk saat ini hiraukan V
:
FingerTree
digambarkan sebagai titk, Finger
sebagai persegi, dan Node
sebagai persegi dalam persegi:
Penambahanan elemen pada bagian depan sebuah FingerTree
dengan +:
efisien
karena Deep
hanya menambah elemen baru pada bagian kiri (left
) dari palem.
Bila palem berupa sebuah Four
, kita akan membangun ulang batang (spine
)
untuk mengambil 3 elemen sebagai sebuah Node3
. Sama halnya dengan penambahan
sebuah elemen pada bagian belakang menggunakan :+
, namun dibalik.
Penambahan menggunakan |+|
dan <++>
lebih efisien bila dibandingkan dengan
menambahkan sebuah elemen satu persatu karena dua pohon Deep
mampu memelihara
cabang bagian luar, membangun batang (spine
) berdasarkan 16 kombinasi yang
mungkin dari dua nilai Finger
pada bagian tengah.
Di atas, kita melewatkan V
. Yang tidak diperlihatkan pada deskripsi
ADT merupakan sebuah implicit measurer: Reducer[A, V]
pada tiap elemen
dari ADT.
Reducer
merupakan sebuah ekstensi dari Monoid
yang memperkenankan agar
sebuah elemen dapat ditambahkan ke sebuah M
Sebagai contoh, Reducer[A, IList[A]]
menyediakan implementasi .cons
yang efisien
6.8.10.1 IndSeq
Bila kita menggunakan Int
sebagai V
, kita bisa mendapatkan barisan terindeks
dimana yang menjadi ukuran adalah jumlah satuan V
. Hal ini memperkenankan
kita untuk melakukan pencarian berdasarkan indeks dengan membandingkan indeks
yang diinginkan dengan ukuran dari tiap cabang pada struktur:
Penggunaan lain dari FingerTree
adalah barisan terurut, dimana yang menjadi
ukuran merupakan nilai terbesar dari setiap cabang:
6.8.10.2 OrdSeq
OrdSeq
tidak mempunyai instans kelas tipe dikarenakan struktur data ini
hanya berguna untuk pembangunan deret terurut secara bertahap dengan duplikat.
Kita dapat mengakses FingerTree
yang melandasi struktur data ini bila dibutuhkan.
6.8.10.3 Cord
Penggunaan FingerTree
yang paling jamak adalah wadah sementara untuk representasi
String
pada Show
. Pembuatan sebuah String
bisa saja ribuan kali lebih cepat
bila dibandingkan dengan implementasi case class
berlapis dari .toString
yang
membangun sebuah Sring
untuk tiap lapisan pada ADT.
Sebagai contoh, instans Cord[String]
mengembalikan sebuah Three
dengan
string pada bagian tengah dan tanda petik pada kedua sisi
Sehingga, sebuah String
memberikan hasil sebagaimana yang tertulis pada
kode sumber
6.8.11 Antrian Prioritas Heap
Antrian prioritas merupakan struktur data yang memperkenankan untuk penyisipan yang relatif singkat pada elemen terurut yang memperbolehkan adanya duplikasi elemen dan memiliki waktu akses yang cepat pada nilai minimum atau prioritas tertinggi. Struktur ini tidak wajibkan untuk menyimpan elemen non-minimal secara berurutan. Implementasi naif dari antrian prioritas dapat berupa
push
bisa sangat cepat (O(1)
) walau reorder
(dan pop
) sangat bergantung
pada IList.sorted
yang bernilai O(n log n)
.
Scalaz menyandikan antrian prioritas dengan struktur pohon dimana setiap simpul
mempunyai nilai kurang dari anaknya. Heap
mempunyai waktu operasi insert
,
union
, size
, uncons, dan
minimumO`:
Heap
diimplementasi dengan Pohon Palem berdasarkan nilai Ranked
dimana
rank
ing merupakan kedalaman dari cabang pohon. Hal ini memperkenankan kita untuk
menyeimbangkan kedalaman dari struktur pohon tersebut. Kita juga mempertahankan
secara manual agar nilai minimum
selalu pada bagian paling atas. Keuntungan
dari penyandian nilai minimum pada struktur data adalah minimumO
adalah biaya
pencarian gratis:
Ketika menyisipkan sebuah catatan, kita membandingkan nilai minimum saat ini dan menggantinya dengan catatan baru bila ternyata lebih rendah:
Penyisipan nilai nilai non-minimal menghasilkan struktur tak-urut pada
cabang minimum. Saat kita menemukan dua atau lebih sub-pohon dengan rank
ing
yang sama, kita akan menempatkan nilai minimum pada bagian depan:
Dengan menghindari pengurutan secara menyeluruh terhadap pohon, insert
menjadi
sangat cepat (dengan kompleksitas O(1)
), dimana yang melakukan operasi ini
tidak terbebani oleh operasi ini. Namun, saat melakukan uncons
dengan deleteMin
,
kita akan mendapati bahwa operasi ini mempunyai kompleksitas O(log n)
yang disebabkan
oleh pencarian nilai minimum dan menghapusnya dari pohon dengan membangun ulang.
Secara umum, hal ini lebih cepat bila dibandingkan dengan implementasi naif.
Operasi union
juga dapat menghambat pengurutan sehingga operasi ini mempunyai
kompleksitas O(1)
.
Bila Order[Foo]
tidak dapat dengat tepat menentukan prioritas yang kita inginkan
atas Heap[Foo]
, kita dapat menggunakan Tag
dan menyediakan instans
Order[Foo @@ Custom]
khusus untuk Head[Foo @@ Custom]
.
6.8.12 Diev
(Interval Diskrit)
Kita dapat dengan mudah menyandikan nilai integer antara 6, 9, 2, 13, 8, 14, 10,
7, 5 sebagai interval inklusif [2, 2], [5, 10], [13, 14]
. Diev
merupakan
metoda penyadian efisien atas interval untuk elemen A
yangc mempunyai instans
kelas tipe Enum[A]
yang akan semuakin efisien bila isi dari struktur data ini
semakin padat.
Saat memutakirkan Diev
, interval yang berdekatan akan digabungkan (dan diurutkan)
sehingga sebuah set nilai akan mempunyai sebuah representasi yang unik.
Salah satu contoh penggunaan untuk Diev
adalah penyimpanan periode waktu.
Sebagai contoh konkret, pada TradeTemplate
kita pada bab sebelumnya.
kita akan menemui bahwa payments
sangat padat, kita mungkin berharap untuk
menggantinya dengan representasi Diev
dengan alasan performa tanpa mengubah
logika bisnis dikarenakan kita menggunakan Monoid
, bukan List
. Walaupun hal
itu berarti kita harus menyediakan instance Enum[LocalDate]
.
6.8.13 OneAnd
Seperti yang sudah dipelajari, Foldable
merupakan pustaka setara untuk
pustaka koleksi dan Foldable1
untuk koleksi non-kosong. Sementara ini, kita
baru melihat NonEmptyList
untuk menyediakan instans Foldable1
. Struktur data
sederhana OneAnd
melapisi semua koleksi lain menkadi Foldable1
:
NonEmptyList
bisa merupakan alias untuk OneAnd[IList]
. Sama halnya dengan
alias dari struktur data ini, kita bisa membuat Stream
, DList
, dan Tree
.
Namun, penggunaan ini dapat menghapus penyusunan dan keunikan dari struktur yang
melandasinya: sebuah OneAnd[ISet, A]
adalah struktur non-kosong dari ISet
.
Namun, elemen pertama dari struktur ini pasti tidak kosong dan bisa jadi juga
merupakan elemen dari ISet
sehingga struktur ini tidak menjadi unik lagi.
6.9 Kesimpulan
Pada bab ini, kita sudah mempelajari secara sekilas tentang tipe data yang ditawarkan oleh Scalaz.
Pembaca yang budiman tidak harus menghafal struktur data yang ada pada bab ini, dan cukup menganggap bab ini sebagai pengantar.
Pada jagad pemrograman fungsional, struktur data fungsional merupakan area riset yang aktif. Publikasi akademis juga sering muncul dengan pendekatan baru atas permasalahan yang sudah lama dikenal. Menerapkan sebuah struktur data fungsional dari literatur semacam itu merupakan kontrubusi yang sangat diterima untuk ekosistem Scalaz.
7. Monad Lanjutan
Untuk menjadi pemrogram dengan aliran fungsional, pembaca budiman harus menguasai beberapa hal, seperti Monad Lanjutan.
Namun, karena kita merupakan pengembang yang mendambakan hal yang sederhana,
juga tidak melupakan bahwa apa yang kita sebut sebagai “lanjutan” juga tetap
sederhana. Sebagai konteks: scala.concurrent.Future
lebih rumit dan penuh
dengan nuansa bila dibandingkan dengan semua Monad
yang ada pada bab ini.
Pada bab ini, kita akan mempelajari beberapa penerapan paling penting atas
Monad
.
7.1 Masa Depan yang Kabur
Masalah paling besar dengan Future
adalah struktur ini segera menjadwalkan
tugas pada saat konstruktsi. Sebagaimana yang telah kita bicarakan pada
perkenalan, Future
menggabungkan antara definisi program dengan
menerjemahkannya.
Dan bila dilihat dari sudut pandang performa, Future
tidak begitu menarik:
setiap kali .flatMap
dipanggil, sebuah closure diserahkan kepada sebuah
Executor
sehingga menyebabkan penjadwalan dan pertukaran konteks yang tak perlu.
Bukan hal yang jarang terjadi bila kita melihat 50% penggunaan CPU saat berurusan
dengan penjadwalan utas, bukan saat melakukan komputasi program. Bahkan, bukan
hal yang tidak mungkin untuk mendapatkan hasil komputasi paralel yang lebih lambat
saat menggunakan Future
.
Bila evaluasi tegas dan penyerahan eksekutor digunakan secara bersamaan, pengguna
tidak akan tahu kapan tugas akan dimulai, selesai, atau sub-tugas yang dibuat
untuk menghitung hasil akhir. Seharusnya, bukan hal yang mengejutkan bila
solusi untuk melakukan pengawasan atas framework yang dibuat berdasarkan
Future
memang pantas disebut sebagai tukang tipe.
Terlebih lagi, Future.flatMap
mengharuskan sebuah ExecutionContext
berada
pada cakupan implisit: pengguna dipaksa untuk memikirkan logika bisnis dan
semantik dari eksekusi pada saat yang bersamaan.
7.2 Efek dan Efek Samping
Bila kita tidak boleh memanggil metoda dengan efek samping pada logika bisnis
kita, atau pada Future
(atau pada Id
, Either
, ataupun Const
, dll),
kapan kita bisa? Tentu jawabannya ada pada Monad
yang menunda eksekusi
sampai pada waktunya Monad
ini diinterpretasi pada titik awal aplikasi.
Mulai dari sini, kita akan merujuk I/O dan mutasi sebagai efek pada dunia luar
yang ditangkap oleh sistem tipe, bukan sistem dengan efek samping tersembunyi.
Implementasi paling sederhana dari sebuah Monad
adalah IO
, yang memformalkan
apa yang telah kita tulis pada bagian perkenalan sebagai:
Metoda .interpret
hanya dipanggil sekali pada titik awal sebuah aplikasi:
Namun, ada dua masalah utama pada IO
sederhana semacam ini:
- dapat menyebabkan stack overflow
- tidak mendukung komputasi paralel.
Kedua masalah ini akan diselesaikan pada bab ini. Namun, serumit apapun
implmentasi internal dari sebuah Monad
, prinsip yang dijabarkan disini tidak
berubah: kita memodularisasi pendefinisian dari sebuah program dan eksekusinya
sehingga kita dapat menangkap efek yang muncul pada penanda tipe, dan pada akhirnya
memperkenankan kita untuk menalar hasil modularisasi program tersebut dan
menghasilkan penggunaan ulang kode yang lebih banyak.
7.3 Keamanan Stack
Pada JVM, setiap pemanggilan metoda menambah sebuah catatan pada stack panggilan
pada Thread
, mirip dengan penambahan sebuah elemen pada bagian depan List
.
Ketika sebuah metoda selesai dipanggil, metoda pada bagian head
akan dibuang.
Jumlah maksimal dari stack panggilan ini ditentukan oleh panji -Xss
ketika
memulai java
. Pemanggilan metoda tail recursive dideteksi oleh komplire
Scala dan catatan panggilan tidak akan ditambahkan. Bila kita mencapai batas,
misal dengan pemanggilan rantai metoda yang sangat banyak, kita akan mendapatkan
sebuah StackOverflowError
.
Sayangnya, tiap panggilan berlapis pada .flatMap
milik IO
, sebuah metoda
akan ditambahkan ke stack. Cara paling mudah untuk menebak apakah metoda ini
akan dijalankan selamanya atau hanya beberapa saat saja, kita bisa menggunakan
.forever
dari Apply
(atasan Monad
):
Scalaz mempunyai sebuah kelas tipe yang dapat diimplementasikan oleh struktur
data yang memiliki instans Monad
bila struktur data tersebut aman dari segi
penggunaan stack: BindRec
yang mumbutuhkan ruang stack konstan untuk
bind
rekursif:
Kita tidak perlu menggunakan BindRec
untk semua program. Namun, kelas tipe ini
penting untuk implementasi umum dari Monad
.
Cara yang digunakan untuk mendapatkan keamanan stack adalah dengan mengkonversi
pemanggilan metoda menjadi rujukan ke sebuah ADT, atau yang dikenal dengan
monad Free
:
TDA Free
merupakan representasi tipe data natural untuk antarmuka Monad
:
-
Return
merepresentasikan.point
-
Gosub
merepresentasikan.bind
/.flatMap
Ketika sebuah TDA mencerminkan argumen yang berhubungan dengan fungsi yang berhubungan, pencerminan ini disebut dengan penyandian Church (dari nama Alonzo Church).
Free
mendapat nama seperti itu karena dapat didapatkan secara cuma-cuma (sebagaimana
dengan “Free Beer”) untuk setiap S[_]
. Sebagai contoh, kita dapat menganggap
S
sebagai alkabar dari Drone
atau Machines
pada bab 3 dan membuat
representasi struktur data dari program kita. Kita akan kembali mempelajari mengapa
hal ini berguna pada akhir bab ini.
7.3.1 Trampoline
Untuk sementara ini, Free
lebih umum daripada yang kita butuhkan. Dengan
mengatur aljabar S[_]
menjadi () => ?
, atau komputasi yang ditangguhkan,
kita mendapatkan struktur Trampoline
dan pada akhirnya dapat menerapkan
Monad
dengan aman
Implementasi BindRec
, .tailrecM
, menjalankan .bind
sampai kita mendapat
sebuah B
. Walau secara teknis hal ini bukan merupakan implmentasi @tailrec
,
implementasi ini menggunakan ruang stack secara konstan karena tiap panggilan
mengembalikan sebuah objek heap dengan rekursi yang dijeda.
Fungsi pembantu yang disediakan untuk membuat sebuah Trampoline
secara
sigap adalah dengan .done
atau bisa juga dibuat dengan sebuah jeda menggunakan
metoda .delay
. Kita juga bisa membuat sebuah Trampoline
dengan menggunakan
Trampoline
*by-name dengan metoda
.suspend`:
Saat kita melihat Trampoline[A]
pada basis kode, kita bisa menggantinya pada
visualisasi mental kita dengan sebuah A
. Hal ini disebabkan oleh penambahan
keamanan stack demi kemurnian komputasi. Kita mendapatkan A
dengan menginterpretasikan
Free
dengan metoda .run
yang telah disediakan.
7.3.2 Contoh: DList
dengan Keamanan Stack
Pada bab sebelumnya, kita mendeskripsikan tipe data DList
dengan
Namun, implementasi yang sesungguhnya adalah seperti berikut:
Kita tidak menggunakan panggilan berlapis pada f
, namun kita menggunakan
Trampoline
yang dibekukan. Interpretasi .run
hanya dilakukan bila memang
benar dibutuhkan, seperti pada toIList
. Perubahan yang dilakukan sebenarnya
sedikit, namun kita berhasil mencapai keamanan stack atas DList
yang dapat
melakukan penggabungan list
dalam jumlah besar tanpa harus memenuhi stack.
7.3.3 IO
dengan Keamanan Stack
Hal yang sama dapat dilakukan untuk mengamankan IO
dengan menggunakan
Trampoline
:
Penerjemah di atas, .unsafePerformIO()
, memang sengaja dinamai seperti itu
untuk menakut-nakuti pengguna agar tidak menggunakannya selain di titik awal
aplikasi.
Sekarang, kita tidak akan mendapat galat mengenai stack overflow:
Penggunaan Trampoline
biasanya menimbulkan penurunan performa bila dibandingkan
dengan rujukan biasa. Hal ini dikarenakan Free
disini adalah dibuat tanpa
biaya, bukan digunakan tanpa biaya.
7.4 Pustaka Transformator Monad
Transformator monad merupakan struktur data yang membungkus nilai yang mendasari dan menyediakan efek monadik.
Sebagai contoh, pada bab 2 kita menggunakan OptionT
agar kita dapat menggunakan
F[Option[A]]
pada komprehensi for
sebagaimana kita menggunakan
F[A]
. Hal semacam ini menambahkan efek dari nilai opsional pada program kita.
Atau bisa juga kita menggunakan MonadPlus
untuk mendapatkan efek yang sama.
Subset tipe data ini dan perpanjangan dari Monad
biasa disebut sebagai
Pustaka Transformator Monad atau Monad Transformer Library (MTL) yang dirangkum
di bawah. Pada bagian ini, kita akan membahas tiap transformator, apa guna
mereka, dan bagaimana cara mereka bekerja.
Efek | Pendasaran | Transformator | Kelas Tipe |
---|---|---|---|
pilihan | F[Maybe[A]] |
MaybeT |
MonadPlus |
galat | F[E \/ A] |
EitherT |
MonadError |
nilai waktu jalan | A => F[B] |
ReaderT |
MonadReader |
jurnal / tugas ganda | F[(W, A)] |
WriterT |
MonadTell |
perubahan kondisi | S => F[(S, A)] |
StateT |
MonadState |
jalan terus saja | F[E \&/ A] |
TheseT |
|
kontrol alur | (A => F[B]) => F[B] |
ContT |
7.4.1 MonadTrans
Tiap transformator mempunyai bentuk umum T[F[_], A]
, dan menyediakan setidaknya
satu instans Monad
dan Hoist
sehingga disebut MonadTrans
:
.liftM
memperkenankan kita untuk membuat sebuah transformator monad bila kita
mempunyai sebuah F[A]
. Sebagai contoh, kita dapat membuat sebuah OptionT[IO, String]
dengan memanggil .liftM[OptionT]
pada sebuah `IO[String].
Mirip dengan .liftM
, .hoist
digunakan untuk transformasi natural.
Secara umum, ada tiga cara untuk membuat sebuah transformator monad:
- dengan menggunakan konstruktor transformator
- dari sebuah nilai
A
dengan menggunakan.pure
dari sintaksMonad
- dari sebuah
F[A] dengan menggunakan
.liftMdari sintaks
MonadTrans`
Dikarenakan cara kerja dari penebak tipe pada Scala, sering kali parameter tipe yang kompleks harus tersurat. Untuk menyiasati hal ini, transformator biasanya menyediakan alat bantu konstruktor pada pasangan, sehingga dapat dengan mudah digunakan.
7.4.2 MaybeT
OptionT
, MaybeT
, dan LazyOptionT
mempunyai implementasi yang mirip.
Mereka sama sama menyediakan opsionalitas melalui Option
, Maybe
, dan LazyOption
.
Kita akan fokus pada MaybeT
untuk menghindari pengulangan pembahasan.
menyediakan sebuah instans untuk MonadPlus
Monad ini memang terlihat agak janggal. Namun, monad ini hanya mendelegasi
semuanya ke Monad[F]
dan pada akhirnya membungkus ulang dengan sebuah MaybeT
.
Hal ini yang disebut dengan pertukangan.
Dengan monad ini, kita dapat menulis logika yang menangani opsionalitas pada
konteks F[]
, tidak dengan membawa-bawa Option
maupun Maybe
.
Sebagai contoh, misalkan kita menggunakan sebuah situs media sosial untuk menghitung
jumlah bintang yang dimiliki oleh seorang pengguna. Situs tersebut memberikan sebuah
String
yang mungkin bisa berisi informasi tentang pengguna dan bisa juga tidak.
Selain itu, kita memilki aljabar berikut:
Kita harus memanggil getUser
dan dilanjutkan dengan getStars
. Bila kita
menggunakan Monad
sebagai konteks dari pemanggilan ini, kita akan kesulitan
menulis fungsi untuk ini karena kita harus menangani kondisi Empty
:
Namun, bila kita mempunyai sebuah MonadPlus
sebagai konteks, kita dapat
memasukkan Maybe
ke dalam F[]
dengan .orEmpty
dan mengabaikan apa yang
terjadi selanjutnya:
Namun, dengan menambahkan persyaratan MonadPlus
, akan muncul permasalah
bila konteks hilir tidak mempunyai instans monad tersebut. Solusi yang bisa
digunakan adalah antara mengganti konteks menjadi MaybeT[F, ?]
(mengangkat
Monad[F]
menjadi MonadPlus
), atau secara tersurat menggunakan MaybeT
pada
tipe kembalian, walaupun harus menulis kode sedikit lebih banyak:
Keputusan untuk menggunakan Monad
atau mengembalikan sebuah transformator
pada akhirnya merupakan hal yang harus diputuskan oleh tim pembaca yang budiman
berdasarkan pada interpreter yang digunakan pada program pembaca budiman.
7.4.3 EitherT
Nilai opsional merupakan sebuah kasus khusus dimana sebuah nilai bisa saja
berupa sebuah galat, namun kita tidak tahu apapun mengenai galat tersebut.
EitherT
(dan varian lundungnya, LazyEitherT
) memperkenankan kita untuk
menggunakan tipe apapun yang kita inginkan sebagai nilai galat beserta
menyediakan informasi kontekstual mengenai apa yang salah.
EitherT
merupakan pembungkus atas sebuah F[A \/ B]
Monad
pada konteks berikut adalah sebuah MonadError
.raiseError
dan .handleError
cukup jelas: keduanya ekuivalen dengan metoda
throw
dan catch
sebuah galat.
MonadError
mempunyai beberapa sintaks tambahan untuk menangani masalah-masalah
umum:
.attempt
mengubah galat menjadi nilai, yang berguna untuk menampakkan galat
pada subsistem sebagai nilai utama.
.recover
digunakan untuk mengubah sebuah galat menjadi nilai untuk semua
kasus yang mungkin terjadi. Sebaliknya, .handleError
menerima sebuah
F[A]
dan pada akhirnya memperkenankan pemulihan sebagian.
.emap
, yang merupakan pemetaan atas either, mengaplikasikan transformasi
yang bisa saja gagal.
MonadError
untuk EitherT
adalah:
Seharusnya bukan hal yang mengejutkan bila kita dapat menulis ulang contoh
dari MonadPlus
dengan menggunakan MonadError
dan menyisipkan pesan galat
yang informatif:
dimana .orError
merupakan metoda bantuan pada Maybe
Versi yang menggunakan EitherT
terlihat sebagai berikut
Instans paling sederhana dari MonadError
adalah \/
yang sangat cocok untuk
testing logika bisnis yang membutuhkan sebuah MonadError
. Sebagai contoh,
Tes unit kita untuk .stars
mungkin mencakup hal berikut:
Sebagaimana yang telah kita saksikan beberapa kali, kita dapat fokus pada testing untuk logika bisnis seutuhnya.
Dan pada akhirnya, kita kembali ke aljabar JsonClient
pada bab 4.3
harap diingat bahwa kita hanya menulis jalur lancar pada API. Bila interpreter
kita untuk aljabar ini hanya bekerja pada F
yang memiliki MonadError
, kita
dapat mendefinisikan jenis galat sebagai permasalahan yang berhubungan.
Dan memang pada kenyataannya, kita dapat mempunyai dua lapis galat bila kita
mendefinisikan interpreter untuk sebuah EitherT[IO, JsonClient.Error, ?]
Yang mencakup masalah I/O, status peladen, dan masalah masalah pada pemodelan dari muatan JSON dari peladen.
7.4.3.1 Memilih Tipe Galat
Komunitas Scalaz masih belum dapat menyimpulkan mengenai strategi terbaik
untuk tipe galat E
di MonadError
.
Salah satu mahzab berpendapat bahwa kita harus memilih yang umum, seperti String
.
Mahzab lain berpendapat bahwa sebuah aplikasi harus mempunyai ADT untuk galat
yang memperkenankan penanganan galat yang disesuaikan. Kaum air di daun talas
sendiri lebih memilih untuk menggunakan Throwable
demi kompatibilitas penuh
atas JVM
.
Ada dua masalah yang muncul bila kita menggunakan ADT galat pada tingkat aplikasi:
- sangat canggung bila kita membuat sebuah galat baru. Satu berkas menjadi lumbung galat utama, mengagregasi galat dari semua subsismet.
- tidak peduli betapa granular galat yang ada, resolusi yang dipakai cenderung sama: catat galat tersebut lalu coba lagi atau berhenti. Kita tidak perlu ADT untuk hal semacam ini.
Sebuah ADT galat menjadi berguna bila tiap catatan menerima penanganan pemulihan yang berbeda.
Sebuah kompromi antara galat ADT dan String
adalah format pertengahan.
JSON merupakan pilihan yang bagus karena format ini dipahami oleh kebanyakan
framework pengawasan dan pencatatan log.
Masalah yang muncul bila kita tidak memiliki stacktrace adalah sulitnya
mencari kode yang menjadi sumber galat. Dengan sourcecode
oleh Li Haoyi,
kita dapat mengikutsertakan informasi kontekstual sebagai metadata pada galat kita:
Walau Err
dapat dirujuk secara transparan, konstruksi implisit dari sebuah
Meta
secara sekilas tidak terlihat bisa dirujuk secara transparan bila
dibaca seperti biasa: dua panggilan ke Meta.gen
(dipanggil secara implisit
saat membuat sebuah Err
) akan menghasilkan nilai yang berbeda karena lokasi
dari kode sumber berhubungan dengan nilai yang dikembalikan:
Untuk memahami hal ini, kita harus mengapresiais bahwa metoda sourcecode.*
merupakan makro yang menggenerasi kode sumber untuk kita. Bila kita harus menulis
kode di atas secara eksplisit, apa yang terjadi akan menjadi jelas:
Betul, kita sudah bersekutu dengan iblis makro, namun kita dapat menulis Meta
secara manual.
7.4.4 ReaderT
Monad pembaca membungkus A => F[B]
sehingga memperkenankan program F[B]
untuk
bergantung kepada nilai waktu-jalan A
. Bagi pembaca yang sudah akrab dengan
penyuntikan dependensi (dependency injection), monad pembaca ekuivalen dengan
anotasi @Inject
milik Spring maupun Guice. Namun, tanpa disertai dengan refleksi
maupun XML.
ReaderT
hanya merupakan alias untuk tipe data yang lebih umum yang dinamai
berdasarkan matematikawan Heinrich Kleisli.
Konversi implicit
pada objek pendamping memperkenankan kita untuk menggunakan
sebuah Kleisli
pada bagian yang seharusnya menjadi tempat untuk sebuah fungsi.
Hal ini memperkenankan kita untuk menggunakan struktur data ini sebagai
parameter pada .bind
atau >>=
dari sebuah monad.
Penggunaan paling jamak untuk ReaderT
adalah sebagai penyedia informasi lingkungan
jalan untuk sebuah progarm. Pada drone-dynamic-agents
, kita membutuhkan akses
untuk OAuth 2.0 Refresh Token milik pengguna agar dapat menghubungi Google.
Tentu hal yang paling mudah dilakukan adalah memuat informasi tersebut dari diska
dan membuat tiap metoda menerima sebuah parameter RefreshToken
. Bahkan,
hal semacam ini merupakan persyaratan umum yang diajukan oleh Martin Odersky
pada proposal implicit function.
Sebuah solusi yang lebih jitu untuk program kita adalah dengan membuat sebuah aljabar yang menyediakan konfigurasi saat dibutuhkan. Misalnya,
Kita sudah membuat ulang MonadReader
, kelas tipe yang berhubungan dekat dengan
ReaderT
, dimana .ask
sama dengan .token
pada potongan diatas, dan S
sebagai
RefreshToken
:
dengan implmentasi
Hukum dari MonadReader
adalah S
tidak boleh berubah diantara tiap pemanggilan.
Sebagai contoh, ask >> ask === ask
. Untuk penggunaan MonadReader
pada program
kita, kita hanya perlu membaca konfigurasi kita satu kali saja. Bila kita ingin
memuat ulang konfigurasi tiap kali kita membutuhkannya, misalkan agar kita dapat
mengubah token tanpa harus menjalankan ulang aplikasi, kita dapat memperkenalkan
ConfigReader
yang tidak mempunyai hukum semacam ini.
Pada implementasi OAuth 2.0 kita, kita dapat memindah Monad
ke metoda:
lalu dilanjutkan dengan melakukan refaktorisasi parameter refresh
agar
menjadi bagian dari Monad
Tiap parameter dapat dipindahkan ke MonadReader
. Yang paling penting untuk pemanggil
adalah saat pemanggil hanya perlu untuk menelisik infromsai ini dari hierarki
pemanggilan paling atas. Dengan ReaderT
, kita tidak perlu menggunakan blok
parameter implicit
sehingga mengurang beban mental saat menggunakan Scala.
Metoda lain pada MonadReader
adalah .local
Kita dapat mengubah S
dan menjalankan sebuah program fa
delam konteks lokal
tersebut dan mengembalikan S
asli. Contoh penggunaan .local
adalah saat
membuat “stack trace” yang sesuai untuk domain kita, pencatatan log berlapis!
Sebagaimana pada struktur data Meta
pada bab sebelumnya, kita mendefinisikan
sebuah fungsi pada titik pemeriksaan:
dan kita dapat menggunakannya untuk membungkus fungsi yang beroperasi pada konteks ini.
akan lolos secara otomatis untuk semua yang tidak ditentukan sebelumnya. Sebuah tambahan kompilasi atau sebuah makro dapat melakukan hal yang sebaliknya, memaksa untuk memilih semuanya.
Bila kita mengakses .ask
, kita dapat melihat jejak langkah bagaimana kita
dipanggil, tanpa harus dikaburkan oleh detail implementasi bytecode.
Hal ini merupakan contoh dari stack trace yang dirujuk secara transparan.
Pengembang yang memilih untuk bermain aman mungkin berharap untuk memecah IList[Meta]
pada ukuran tertentu untuk menghindari sesuatu yang mirip dengan stack overflow.
Dan memang pada kenyataannya, struktu data yang cocok adalah Dequeue
.
.local
juga dapat digunakan untuk mencatat informasi kontekstual yang relevan
secara langsung pada tugas saat itu, seperti jumlah spasi yang harus digunakan
untuk melekuk sebuah baris saat mencetak format berkas yang dapat dibaca manusia
dengan mudah. Misal, menambah dua spasi ketika kita memasuki sebuah struktur
berlapis.
Dan paling penting, bila kita tidak dapat meminta sebuah MonadReader
karena
aplikasi kita tidak menyediakannya, kita dapat mengembalikan sebuah ReaderT
Bila sebuah pemanggil menerima ReaderT
dan mereka mempunyai parameter token
,
mereka dapat memanggil access.run(token)
dan mendapatkan sebuah F[BearerToken]
.
Terus terang, karena kita tidak mempunyai banyak pemanggil, kita hanya perlu mengubah
sebuah parameter fungsi. MonadReader
paling berguna saat:
- kita ingin melakukan refaktor kode suatu saat untuk memuat ulang konfigurasi
- nilai tidak dibutuhkan oleh pemanggil perantara
- atau kita ingin menentukan cakupan beberapa variabel secara lokal
Dotty boleh saja tetap menggunakan fungsi implisit, karena kita mempunyai ReaderT
dan MonadReader
.
7.4.5 WriterT
Yang menjadi kebalikan dari pembacaan adalah penulisan nilai. Transformator monad
WriterT
biasanya digunakan untuk menulis ke sebuah jurnal.
Tipe yang dibungkus adalah F[(W, A)]
dengan jurnal yang terakumulasi pada W
.
Tidak hanya satu monad yang berhubungan dengan WriterT
, namun ada 2.
MonadTell
dan MonadListen
MonadTell
digunakan untuk menulis pada jurnal sedangkan MonadListen
digunakan
untuk memperoleh nilai yang sudah ditulis. Implementasi dari WriterT
adalah
sebagai berikut
Contoh paling jelas adalah dengan menggunakan MonadTell
untuk pencatatan log
ataupun pelaporan audit. Dengan menggunakan ulang Meta
dari pelaporan galat,
kita dapat membayangkan untuk membuat struktur log sebagai berikut
dan menggunakan Dequeue[Log]
sebagai tipe jurnal kita. Kita dapat mengganti
metoda authenticate
OAuth2 kita menjadi
Kita juga bisa menggabungkannya dengan bekas jejak dari ReaderT
untuk mendapatkan
log terstruktur.
Pemanggil dapat mengembalikan log dengan menggunakan .written
dan bebas melakukan
apapun dengannya.
Namun, ada sebuah argumen kuat yang menyatakan bahwa pencatatan log berhak mendapatkan aljabarnya sendiri. Pembagian tingkat log seringkali dibutuhkan dengan alasan performa. Dan sering kali, penulisan log dilakukan pada tingkat aplikasi, bukan pada komponen.
W
pada WriterT
mempunyai sebuah Monoid
yang memperkenankan kita untuk
mencatat semua jenis kalkulasi monoidik sebagai nilai sekunder bersamaan dengan
program utama kita. Sebagai contoh, menghitung berapa kali kita melakukan sesuatu,
membangun sebuah penjelasan dari sebuah kalkulasi, ataupun membangun sebuah
TradeTemplate
untuk trade baru saat kita menakar harganya.
Spesialisasi yang populer dari WriterT
adalah saat monad yang digunakan adalah
Id
, yang juga berarti bahwa nilai run
yang melandasinya hanyalah merupakan
sebuah tuple sederhana (W, A)
.
yang memperkenankan kita agar nilai apapun dapat membawa kalkulasi monoidal kedua
tanpa harus membutuhkan konteks F[_]
.
Singkat kata, WriterT
/ MonadTell
merupakan cara untuk melakukan tugas-ganda
pada pemrograman fungsional.
7.4.6 StateT
StateT
memperkenankan kita untuk melakukan .put
, .get
, dan .modify
pada
sebuah nilai yang sedang ditangani pada konteks monadik. Monad ini merupakan
pengganti var
pada pemrograman fungsional.
Bila kita harus menulis sebuah metoda tak murni yang mempunyai
akses ke beberapa kondisi yang tidak tetap dan disimpan pada sebuah var
, metoda
ini mungkin mempunyai penanda () => F[A]
dan mengembalikan nilai yang berbeda
pada tiap kali pemanggilan dan pada akhirnya mengaburkan perujukan. Dengan
pemrograman fungsional murni, fungsi tersebut menerima sebuah keadaan
(state) sebagai masukan dan mengembalikan keadaan yang termutakhirkan sebagai
keluaran. Ini-lah yang menjadi pendasaran mengapa tipe dasar dari StateT
adalah
S => F[(S, A)]
.
Monad yang terasosiasi dengan StateT
adalah MonadState
StateT
diimplementasikan sedikit berbeda dengan transformator monad yang sudah
kita pelajari sampai saat ini. StateT
bukan berupa case class
, namun merupakan
sebuah ADT yang berisi dua anggota:
yang merupakan bentuk khusus dari Trampoline
dan memberikan kita keamanan
stack bila kita ingin mengembalikan struktur data standar dengan .run
:
StateT
dapat dengan mudah mengimplementasikan MonadState
dengan ADT-nya:
With .pure
mirrored on the companion as .stateT
:
dan MonadTrans.liftM
menyediakan konstruktor F[A] => StateT[F, S, A]
.
Varian umum dari StateT
adalah saat F = Id
yang memberikan tipe dasar sebagai
S => (S, A)
. Scalaz menyediakan sebuah alias tipe dan fungsi pembantu untuk
berinteraksi dengan transformator monad State
secara langsung, dan mencerminkan
MonadState
:
Sebagai contoh, kita dapat kembali ke tes logika bisnis dari drone-dynamic-agents
.
Harap diingat kembali pada bab 3 kita telah membuat Mutable
sebagai penerjemah
tes untuk aplikasi kita dan menyimpan perhitungan started
dan stoped
pada sebuah
var
.
Sekarang kita tahu bahwa kita dapat membuat simulator tes yang jauh lebih baik
dengan menggunakan State
. Kita akan menggunakan kesempatan ini untuk meningkatkan
akurasi dari simulasi tersebut. Mohon diingat bahwa objek domain utama kita merupakan
pandangan aplikasi kita terhadap dunia luar:
Karena kita menulis simulasi dari dunia luar untuk tes kita, kita dapat menulis sebuah tipe data yang membawa nilai-nilai kebenaran untuk aplikasi kita
Pembeda utama adalah simpul started
dan stopped
dapat dipisahkan.
Penerjemah kita dapat diimplementasikan menggunakan State[World, a]
dan kita
dapat menulis tes kita untuk memeriksa bagaimanakah bentuk dari World
dan
WorldView
setelah logika bisnis berjalan.
Penerjemah, yang meniru penghubungan layanan eksternal Drone dan Google, dapat diimplementasikan seperti berikut:
dan kita dapat menulis ulang tes kita agar mengikuti konvensi dimana:
-
world1
merupakan keadaan dunia luar sebelum program berjalan -
view1
merupakan apa yang aplikasi kita ketahui tentang dunia luar -
world2
merupakan keadaan dunia luar setelah program berjalan -
view2
merupakan apa yang aplikasi kita ketahui tentang dunia luar setelah program berjalan
Sebagai contoh,
Mungkin akan dimaafkan bila kita melihat kembali ikalan logika bisnis kita
dan menggunakan StateT
untuk mengatur state
. Namun, logika bisnis DynAgents
kita hanya membutuhkan Applicative
dan kita akan melanggar Rule of Least Power
yang meminta kuasa lebih dari MonadState
. Jadi, cukup masuk akal bila kita
menangani keadaan secara manual dengan melemparnya secara langsung ke update
dan act
, dan membiarkan siapapun yang ingin memanggil kita dengan menggunakan
StateT
, bila itu yang mereka inginkan.
7.4.7 IndexedStateT
Kode yang telah kita pelajari selama ini masih belum menunjukkan bagaimana
Scalaz mengimplementasikan StateT
. Dan pada kenyataannya, StateT
hanya
berupa alias tipe untuk IndexedStateT
Implementasi dari IndexedStateT
kurang lebih sama dengan dengan apa yang telah
kita pelajari sampai pada bab ini, dengan beberapa tambahan parameter tipe
yang memperbolehkan agar masukan S1
dan keluaran S2
berbeda:
IndexedStateT
tidak mempunyai instans MonadState
bila S1 != S2
, walaupun
mempunyai Monad
.
Contoh berikut diadaptasi dari presentasi Index Your State
oleh Vincent Marquez. Bayangkan sebuah skenario dimana kita harus mendesain antarmuka
aljabaris untuk sebuah pencarian String
berdasarkan sebuah Int
. Antarmuka ini
bisa saja mempunyai implementasi yang berhubungan dengan implementasi lainnya
dan urutan panggilan sangat penting. Percobaan pertama kita mungkin akan terlihat
seperti berikut:
dengan galat waktu-jalan bila .update
atau .commit
dipanggil tanpa sebuah
.lock
. Desain yang lebih kompleks mungkin menggunakan beberapa trait dan
DSL khusus yang tidak ada yan mengingat tentangnya.
Atau, kita bisa menggunakan IndexedStateT
yang memaksa pemanggil memang pada
tempat yang tepat. Pertama, kita mendefinisikan keadaan yang mungkin sebagai
sebuah ADT
dan memeriksa kembali aljabar kita
yang akan memberikan galat waktu-kompilasi bila kita mencoba untuk melakukan
.update
tanpa .lock
namun memperkenankan kita untuk membuat fungsi yang dapat dikomposisi dengan mengikutsertakannya secara tersurat:
7.4.8 IndexedReaderWriterStateT
Bagi pembaca yang menginginkan untuk menggabungkan ReaderT
, WriterT
, dan
IndexedStateT
dapat menggunakan IndexedReaderWriterStateT
yang mempunyai
penanda tipe (R, S1) => F[(W, A, S2)]
dengan R
yang memiliki semantik Reader
,
W
memiliki semantik penulisan monoidik, dan S
untuk pembaruan keadaan terindeks.
Singkatan disediakan karena bila tidak, tidak ada yang mau menulis kata sepanjang itu:
IRWST
merupakan implementasi yang lebih efisien bila dibandingkan dengan
membuat transformator stack dari ReaderT[WriterT[IndexedStateT[F, ...], ...], ...]
secara manual.
7.4.9 TheseT
TheseT
memperkenankan agar galat dapat diakumulasi bila ada beberapa komputasi
berhasil diselesaikan atau untuk membatalkan komputasi secara keseluruhan.
The underlying data type is F[A \&/ B]
with A
being the error type,
requiring a Semigroup
to enable the accumulation of errors.
Tidak ada monad khusus yang diasosiasikan dengan TheseT
karena TheseT
hanya
merupakan Monad
biasa. Bila kita ingin membatalkan sebuah kalkulasi, kita dapat
mengembalikan nilai This
. Namun, bila kita ingin mengakumulasi galat, kita harus
mengembalikan sebuah Both
yang juga berisi bagian komputasi yang berhasil
diselesaikan.
TheseT
juga bisa dilihat dari sudut pandang lain: A
tidak harus berupa sebuah
galat. Hal yang sama dengan Writer
, A
bisa saja berupa hasil kalkulasi kedua
yang kita proses bersama dengan kalkulasi utama B
. TheseT
memperkenankan
pemutusan dini bila sesuatu yang tak biasa terjadi pada A
dan mengaharuskannya.
Sebagaimana ketika Charlie Bucket menemukan tiket emas (A
), dia membuang
batang coklatnya (B
).
7.4.10 ContT
Continuation Passing Style merupakan gaya pemrograman dimana fungsi tidak pernah mengembalikan nilai, namun melanjutkan komputasi selanjutnya. CPS populer pada Javascript dan Lisp karena gaya ini memperkenankan operasi I/O asinkronus melalui panggilan balik saat data tersedia. Penulisan ulang untuk pola semacam ini pada Scala dengan gaya tidak murni kurang lebih seperti ini:
Kita dapat membuatnya menjadi murni dengan memperkenalkan konteks
F[_]
dan melakukan refaktor agar mengembalikan sebuah fungsi yang menerima masukan yang disediakan
ContT
sebenarnya hanya berupa kontainer untuk penanda ini, dengan sebuah instans
Monad
dan sintaks pembantu untuk membuat sebuah ContT
dari sebuah nilai monadik:
Namun, penggunaan panggilan ulang sederhana untuk continuation
tidak memberikan apapun untuk pemrograman fungsional murni karena
kita sudah mengetahui bagaimana mengurutkan komputasi asinkoronus yang memungkinkan
untuk didistribusi dengan menggunakan Monad
beserta bind
atau panah Kleisli
.
Agar kita dapat melihat mengapa continuation berguna, kita harus memperhitungkan
contoh yang lebih kompleks pada batasan desain yang lebih kaku.
7.4.10.1 Kontrol Alur
Misalkan, bila kita telah memodularkan aplikasi kita menjadi beberapa komponen yang dapat melakukan operasi I/O, dan tiap komponen dimiliki oleh tim pengembang lain:
Tujuan kita adalah menghasilkan sebuah A0
bila kita memiliki sebuah A1
.
Bila Javascript dan Lisp akan memilih untuk menggunakan kontinyuasi untuk
menyelesaikan masalah ini (karena operasi I/O dapat mencegah operasi lainnya
dijalankan), kita cukup merangkai fungsi-fungsi di atas
Kita dapat mengangkat .simple
menjadi bentuk kontinyuasi dengan menggunakan
sintaks pembantu, .cps
, dan sedikit plat cetak untuk tiap
langkah:
Jadi, apa yang kita dapatkan dari perubahan diatas? Pertama, alur eksekusi aplikasi ini berjalan dari kiri ke kanan
Bila kita merupakan penulis untuk foo2
dan ingin melakukan pemrosesan lebih
lanjut terhadap a0
yang kita terima dari bagian kanan, misal kita ingin memecah
menjadi foo2a
dan foo2b
Juga jangan lupa untuk menambah batasan bahwa kita tidak dapat mengubah definisi
dari flow
atau bar0
. Bisa jadi karena keduanya bukan kode kita maupun sudah
ditentukan oleh framework yang kita gunakan.
Kita juga tidak bisa memproses keluaran dari a0
dengan mengubah metoda barX
lainnya. Namun, dengan ContT
kita dapat mengubah foo2
agar memproses hasil
dari kontinyuasi selanjutnya (next
):
Yang bisa kita definisikan sebagai
Kita tidak hanya bisa untuk menggunakan .map
pada nilai kembalian, namun juga
bisa melakukan menempelkan .bind
pada kontrol alur lain. Sehingga mengubah alur
linier menjadi sebuah graf.
Atau kita tetap menggunakan alur eksekusi yang lama dan mengulangi semua eksekusi hilir
Potongan diatas hanya melakukan perulangan sekali saja, tidak tak hingga. Sebagai contoh, kita mungkin meminta operasi hilir untuk mengkonfirmasi ulang sebuah operasi yang mungkin berbahya.
Pada akhirnya, kita dapat melakukan operasi yang khusus untuk konteks dari
ContT
, dalam kasus ini IO
, yang memperkenankan kita untuk menangani galat
dan membersihkan sumber daya komputasi:
7.4.10.2 Saat Nemu Benang Kusut
Bukanlah sebuah kebetulan bila diagram-diagram diatas terlihat seperti benang kusut.
Hal semacam ini memang terjadi bila kita main-main dengan kontrol alur. Semua
mekanisme yang telah kita diskusikan pada bagian ini memang mudah diterapkan
bila kita dapat menyunting definisi dari flow
, sehingga kita tidak perlu
menggunakan ContT
.
Namun, bila kita merancang sebuah framework, kita harus mempertimbangkan
penyingkapan sistem plugin karena panggilan balik ContT
memperkenankan
pengguna untuk lebih leluasa mengkontrol alur program mereka. Dan memang
kenyataanya, kadang kala pengguna memang ingin main benang kusut.
Sebagai contoh, bila kompilator Scala ditulis menggunakan CPS, kompilator tersebut akan memperkenankan pendekatan yang jelas dalam komunikasi antar fase kompilasi. Sebuah plugin kompilator akan mampu melakukan beberapa hal berdasarkan hasil penebakan dari tipe sebuah ekspresi yang dikomputasi pada tahap selanjutnya di proses kompilasi. Hal yang sama, kontinyuasi bisa jadi API yang baik untuk penyunting teks ataupun alat bangun yang luwes.
Kekurangan ContT
adalah tidak terjaminnya keamanan stack. Hal ini menyebabkan
ContT
tidak dapat digunakan untuk program yang berjalan selamanya.
7.4.10.3 Keren, Tong. Jangan Pegang ContT
.
Varian yang lebih kompleks dari ContT
adalah IndexedContT
yang membungkus
(A => F[B]) => F[C]
. Parameter tipe baru C
memperkenankan untuk tipe
pengembalian dari komputasi berbeda pada tiap komponennya. Namun, bila B
tidak
setara dengan C
maka Monad
tidak ada.
Tanpa melewatkan kesempatan untuk menggeneralisasi sebanyak mungkin, IndexedContT
sebenarnya diimplmentasikan dalam struktur yang bahkan lebih general. Harap
diperhatikan bahwa ada huruf s
sebagai penanda jamak sebelum huruf T
dimana W[_]
mempunyai sebuah Comonad
dan ContT
diimplementasikan sebagai
sebuah alias tipe. Objek pendamping tersedia untuk alias tipe ini sebagai
konstruktor pembantu.
Memang, lima parameter tipe agak berlebihan dalam penggeneralisasian. Namun, penggeneralisasian yang berlebihan konsisten dengan kontinyuasi.
7.4.11 Susunan Transformer dan Implisit Ambigu
Sub-sub-bab ini menututup perbincangan kita mengenai transformator monad pada Scalaz.
Saat beberapa transformator digabungkan, kita memanggil hasil penggabungan ini
sebagai susunan transformator. Walaupun lantung, sangat memungkinkan untuk
mengetahui fiturnya dengan membaca transformator yang ada. Sebagai contoh, bila
kita membangun sebuah konteks F[_]
yang merupakan set dari transformator
yang digabungkan, seperti
kita tahu bahwa kita menambah penanganan galat dengan tipe galat E
(ada monad
MonadError[Ctx, E]
dan kita mengatur keadaan A
(ada MonadState[Ctx, S]
).
Namun, ada beberapa kekurangan dari sisi praktik bila menggunakan transformator
monad dan kelas tipe Monad
pasangannya:
- Beberapa parameter
Monad
implisit mengakibatkan kompilator tidak dapat menentukan sintaks yang tepat untuk konteks tersebut. - Secara umum, monad tidak dapat digabungkan. Hal ini berarti bahwa urutan pelapisan transformator sangat penting.
- Semua interpreter harus diangkat ke konteks umum. Sebagai contoh, mungkin
saja kita mempunyai sebuah implementasi dari aljabar yang menggunakan
IO
dan kita harus membungkusnya denganStateT
danEitherT
, walau kedua transformator tersebut tidak digunakan di dalam interpreter. - Akan ada beban performa yang harus dibayar untuk tiap lapis. Dan beberapa
transformator monad meminta biaya lebih bila dibandingkan monad lain
terutama
StateT
. Bahkan,EitherT
dapat menyebabkan masalah alokasi memori untuk aplikasi dengan keluaran tinggi.
Maka dari itu, kita harus membahas penyiasatannya
7.4.11.1 Tanpa Sintaks
Misal kita punya sebuah aljabar
dan beberapa tipe data
yang akan kita gunakan pada logika bisnis kita
Masalah pertama yang kita temui adalah potongan kode ini gagal dikompilasi
Ada beberapa solusi untuk masalah ini. Yang paling jelas adalah membuat semua parameter menjadi eksplisit
dan mengharuskan hanya Monad
yang bisa dilewatkan secara implisit melalui
batasan konteks. Namun, hal ini berarti kita harus menyambungkan MonadError
dan MonadState
secara manual ketika memanggil foo1
dan saat memanggil metoda
lain yang meminta sebuah implicit
Solusi kedua adalah menghilangkan parameter implicit
dan menggunakan pembayangan
nama agar semua parameter menjadi eksplisit dengan satu pengecualian. Hal ini
memperkenankan operasi hulu untuk menggunakan resolusi implisit saat memanggil
aljabar ini walaupun kita harus tetap mengumpankan parameter secara eksplisit
bila aljabar ini dipanggil.
bila kita dapat melakukan pembayangan hanya satu monad saja, atau dengan kata lain menyerahkan sintaks pada monad lain, dan baru menghapus pembayangan tersebut saat aljabar ini dipanggil oleh metoda lain
Pilihan ketiga, walaupun lebih berat di awal, adalah dengan membuat kelas tipe
Monad
khusus yang membawa rujukan implicit
ke dua kelas Monad
yang kita
pilih
dan sebuah derivasi dari kelas tipe berdasarkan sebuah MonadError
dan
MonadState
Sekarang, bila kita ingin mengakses S
atau E
, kita bisa mendapatkannya
dengan F.S
maupun F.E
Sebagaimana halnya dengan solusi kedua, kita bisa memilih salah satu dari
instans Monad
dan menjadikannya sebagai konteks implicit
dalam blok,
kita dapat melakukannya mengimpornya
7.4.11.2 Menyusun Transformator
EitherT[StateT[...], ...]
memiliki sebuah instans MonadError
namun tidak
mempunyai MonadState
. Sedangkan StateT[EitherT[...], ..]
mampu menyediakan
keduanya.
Untuk menyiasati hal tersebut, kita dapat mempelajari derivasi implisit pada objek pendamping dari transformator tersebut dan memastikan bahwa transformator paling luar menyediakan semua yang kita butuhkan.
Patokan yang dipakai adalah semakin kompleks sebuah transformator, semakin luar tempat transformator tersebut berada pada susunan. Bab ini akan menyajikan transformator yang semakin tinggi tingkat kompleksitasnya.
7.4.11.3 Mengangkat Penerjemah
Melanjutkan contoh yang sama, misalkan aljabar Lookup
kita memiliki interpreter
IO
namun kita menginginkan agar konteks kita seperti
agar dapat memberi kita sebuah MonadError
dan MonadState
. Hal ini berarti
kita harus membungkus LookupRandom
agar dapat beroperasi pada Ctx
.
Pertama, kita akan menggunakan sintaks .liftM
pada Monad
yang memberikan
MonadTrans
sehingga dapat mengangkat F[A]
menjadi G[F, A]
Yang penting untuk diperhatikan adalah parameter tipe untuk .liftM
mempunyai
dua celah tipe dengan bentuk _[_]
dan _
. Bila kita membuat alias tipe dengan
bentuk seperti
Kita dapat mengabstraksi MonadTrans
agar mengangkat Lookup[F]
menjadi
Lookup[G[F, ?]]
dimana G merupakan Transformator Monad:
Memperkenankan kita untuk membungkus EitherT
satu kali, dan kemudian
membungkus StateT
Cara lain untuk mencapai ini dalam satu langkah adalah dengan menggunakan
MoonadIO
yang mampu mengangkat sebuah IO
menjadi sebuah susunan transformator:
dengan instans MonadIO
untuk semua kombinasi umum dari transformator.
Plat cetak berlebih untuk mengangkat sebuah interpreter IO
menjadi semua monad
apapun yang mempunyai instans MonadIO
hanya dua baris kode untuk definisi
interpreter, ditambah satu baris untuk tiap elemen dari aljabar, dan satu baris
terakhir untuk memanggilnya.
7.4.11.4 Performa
Masalah paling besar pada Transformator Monad adalah tambahan beban sehingga
performa menurun. Walaupun EitherT
memiliki tambahan beban yang kecil, tiap
kali pemanggilan .flatMap
akan membuat banyak objek. Dampak dari hal seperti
ini akan terlihat pada aplikasi yang mempunyai keluaran besar dimana tiap alokasi
objek turut andil dalam gambaran besar. Transformator lain, seperti StateT
,
akan menambah trampolin yang juga tak kecil bebannya. Terlebih lagi untuk
transformator ContT
yang menahan semua rantai panggilan pada memori.
Bila performa menjadi masalah, maka solusi satu-satunya adalah dengan tidak
menggunakan Transformator Monad. Atau setidaknya struktur data transformator.
Keuntungan paling besar dari kelas tipe Monad
, seperti MonadState
adalah
kita dapat membuat konteks F[_]
yang teroptimasi untuk aplikasi kita yang
menyediakan kelas tipe secara alami. Kita akan mempelajari bagaimana cara untuk
membuat sebuah konteks F[_]
yang optimal pada dua bab selanjutnya pada saat
kita membahas mengenai dua struktur data yang sudah kita lihat sebelumnya:
Free
dan IO
.
7.5 Makan Gratis
Industri perangkat lunak sangat menginginkan bahasa pemrograman tingkat tinggi yang memberikan jaminan keamanan sedangkan pengembang trading menginginkan efisiensi dan keandalan dengan performa waktu-jalan yang tinggi.
Kompiler tepat waktu (KTM) pada JVM bekerja dengan sangat baik sampai pada tahap fungsi-fungsi sederhana dapat mempunyai performa yang setara dengan ekuivalen yang ditulis pada bahasa pemrograman C maupun C++, bila mengabaikan beban pada pengumpulan sampah. Namun, KTM hanya bekerja pada optimisasi tingkat rendah seperti: prediksi cabang operasi, inline fungsi, membuka ikalan, dan sejenisnya.
KTM tidak melakukan optimasi pada logika bisnis kita, sebagai contoh, pengelompokan panggilan jaringan atau paralelisasi tugas tugas independen. Pengembang bertanggung jawab untuk menulis logika bisnis dan optimasi pada saat yang bersamaan sehingga menyebabkan penurunan keterbacaan dan mempersulit pemeliharaan. Akan sangat bagus bila optimasi menjadi perhatian tangensial.
Bila kita memiliki struktur data yang mendeskripsikan logika bisnis kita pada konsep tingkat tinggi, bukan instruksi mesin, kita dapat melakukan optimasi tingkat tinggi. Struktur data semacam ini biasanya disebut struktur data Free dan dapat dibuat tanpa membayar apapun untuk anggota dari antarmuka aljabarik dari program kita. Sebagai contoh, sebuah Free Applicative dapat dibuat sehingga kita dapat mengelompokkan atau penghapusan duplikasi atas I/O jaringan intensif.
Pada bagian ini, kita akan mempelajari cara untuk membuat struktur data free (gratis) dan cara penggunaannya.
7.5.1 Free
(Monad
)
Pada dasarnya, sebuah monad mendeskripsikan program berurutan dimana setiap tahap bergantung pada tahap sebelumnya. Maka dari itu, kita tidak bisa serta-merta mengubah sesuatu yang hanya tahu apa yang telah dijalankan dan apa yang akan dijalankan.
Sebagai pengingat, Free
merupakan representasi struktur data dari sebuah Monad
dan didefinisikan dengan tiga anggota
-
Suspend
merepresentasikan sebuah program yang belum diinterpretasi -
Return
sama dengan.pure
-
Gosub
sama dengan.bind
Sebuah Free[S, A]
dapat digenerasi secara cuma-cuma untuk semua aljabar S
.
Agar lebih jelas, anggap aljabar Machines
pada aplikasi kita
Kita mendefinisikan Free
yang dibuat secara cuma cuma untuk Machine
dengan
membuat GADT dengan tipe data untuk tiap elemen dari aljabar. Tiap tipe data
mempunyai parameter masukan yang sama dengan elemen yang sesuai dan diparemeterisasi
atas nilai kembalian dengan nama yang sama:
GADT yang mendefinisikan Pohon Sintaks Abstrak (PSA) karena tiap anggota merepresentasikan sebuah komputasi pada sebuah program.
Lalu kita akan mendefinisikan .liftF
, sebuah implementasi dari Machines
dengan
Free[AST, ?]
sebagai konteksnya. Setiap metoda cukup mendelegasi ke Free.liftT
untuk membuat sebuah Suspend
Saat kita membangun program kita yang terparametrisasi atas sebuah Free
, kita
menjalankannya dengan menyediakan sebuah interpreter (transformasi natural
Ast ~> M
) ke metoda .foldMap
. Sebagai contoh, bila kita dapat menyediakan
sebuah interpreter yang memetakan ke IO
, kita dapat membangun sebuah program
IO[Unit]
dengan menggunakan PSA free.
Agar lebih lengkap, sebuah interpreter yang mendelegasikan kepada sebuah implementasi
langsung biasanya mudah dalam penulisan. Hal ini mungkin berguna bila bagian aplikasi
yang lain menggunakan Free
sebagai konteks dan kita juga sudah mempunyai
implementasi IO
yang ingin kita gunakan:
Namun, logika bisnis kita butuh lebih dari Machines
, kita juga butuh akses ke
aljabar Drone
seperti ini
Yang kita inginkan adalah PSA kita menjadi sebuah kombinasi dari PSA Machines
dan Drone
. Kita telah mempelajari Coproduct
pada bab 6 yang merupakan
sebuah disjungsi jenis tinggi:
Kita dapat menggunakan konteks Free[Coproduct[Machines.Ast, Drone.Ast, ?], ?]
.
Kita juga bisa saja membuat ko-produk secara manual, namun kita akan mempunyai plat cetak yang terlalu banyak. Selain itu, kita harus melakukannya berulang kali bila kita ingin menambah aljabar ketiga.
Kelas tipe scalaz.Inject
membantu:
Derivasi implicit
menghasilkan instans Inject
saat kita membutuhkannya.
Hal ini memperkenankan kita untuk menulis ulang liftF
agar dapat beroperasi
pada semua kombinasi dari PSA:
Sungguh apik bila F :<: G
dibaca sebagaimana bila Ast
sebagai salah satu anggota
dari set instruksi lengkap dari F
.
Dan menggabungkan semuanya, misalkan kita mempunyai sebuah program yang kita tulis
untuk mengabstraksi Monad
dan kita mempunyai implementasi dari Machines
dan Drone
yang sudah ada,
kita dapat membuat interpreter dari implementasi tersebut:
dan menggabungkannya menjadi sebuah set instruksi dengan menggunakan metoda bantuan
dari pasangan NaturalTransformation
Lalu menggunakannya untuk menghasilkan IO
Nah, kita jadi berputar-putar! Kita bisa saja menggunakan IO
sebagai konteks
program kita dan menghindari Free
. Lalu, kenapa kita harus seperti ini?
Berikut merupakan beberapa contoh dimana Free
bisa jadi berguna.
7.5.1.1 Testing: Tiruan dan Potongan
Mungkin terlihat tidak masuk akal bila kita mengusulkan untuk menggunakan Free
agar kita dapat mengurangi plat cetak namun, di sisi lain, kita telah menulis
kode yang sangat banyak yang berkaitan dengan Free
sendiri. Akan tetapi,
ada titik kritis dimana Ast
melunasi semua biaya yang telah kita tulis
saat kita mempunyai banyak tes yang membutuhkan banyak potongan implementasi
kode.
Bila .Ast
dan .liftF
didefinisikan untuk sebuah aljabar, kita dapat membuat
interpreter parsial
yang dapat digunakan untuk mengetes program kita
Dengan menggunakan fungsi parsial, dan bukan fungsi total, kita memaparkan diri kita pada galat waktu-jalan. Banyak tim yang dengan ringan hati menerima risiko ini pada tes unit mereka karena tes akan gagal bila pemrogram melakukan kesalahan.
Walaupun kita juga bisa mencapai hal yang sama dengan menulis implementasi dari
aljabar kita yang mengimplementasikan setiap metoda dengan ???
dan mengesampingkan
apa yang kita butuhkan sesuai dengan per kasus.
7.5.1.2 Monitoring
Sudah pada umumnya ketika aplikasi peladen diawasi dengan agen waktu-jalan yang memanipulasi bytecode untuk menyisipkan profiler dan mengekstrak informasi penggunaan dan performa.
Bila konteks dari aplikasi kita adalah Free
, kita tidak perlu menggunakan
manipulasi bytecode. Kita dapat mengimplementasikan monitor dengan efek samping
sebagai sebuah interpreter yang bisa kita atur sepenuhnya.
Sebagai contoh, misal penggunaan “agen” Ast ~> Ast
yang mencatat metoda penyelawatan: kita bisa menggunakan rutin dari vendor khusus pada kode yang nyata digunakan di produksi atau kita bisa melihat pesan khusus yang kita inginkan dan mencatatnya sebagai alat bantu debug.
Kita dapat menempelkan Monitor
ke aplikasi Free
kita yang sudah ada di
tahap produksi dengan
or combine the natural transformations and run with a single
atau menggabungkannya dengan transformasi natural dan menjalankannya dengan sebaris
7.5.1.3 Tambal Ban
Sebagai tenaga ahli, kita terbiasa dengan permintaan penyiasatan aneh yang akan ditambahkan pada logika utama dari aplikasi. Mungkin juga kita ingin mengkodifikasi kasus di luar parameter normal sebagai sebuah pengecualian dan menanganinya sesuai dengan logika inti kita.
Sebagai contoh, misalkan kita mendapat memo dari bagian keuangan yang berisi
*PENTING: Bob menggunakan simpul
#c0ffee
untuk menjalankan laporan keuangan akhir tahun. JANGAN MATIKEUN MESINNYA BEGO!!SEBELAS111
Sangat tidak mungkin untuk mendiskusikan mengapa Bob tidak boleh menggunakan mesin kita untuk keperluan akuntansinya yang sangat penting. Jadi, kita harus membedah logika bisnis kita dan menelurkan sebuah rilis ke tahap produksi secepat mungkin.
Tambalan kita dapat dipetakan menjadi sebuah struktur Free
yang memperkenankan
kita untuk mengembalikan sebuah hasil yang sudah jadi (Free.pure
), bukan
instruksi terjadwal. Kita mengkhususkan instruksi tersebut pada sebuah transformasi
natural dengan nilai kembalian:
pastikan sepasti-pastinya bahwa kode di atas memang benar berjalan sesuai keinginan, lalu gunakan di lingkungan produksi, atur alarm agar minggu depan untuk mengingatkan agar kita hapus kode ini, dan hapus akses Bob ke server kita.
Tes unit kita dapat menggunakan State
sebagai konteks target, sehingga kita
dapat melacak semua simpul yang kita hentikan:
juga dengan tes untuk simpul “normal” yang tidak kita hentikan.
Keuntungan menggunakan Free
untuk menghindari penghentian simpul #c0ffee
adalah kita dapat memastikan bahwa semua penggunaan tercatat, bukan harus mencari
logika bisnis dan mencari penggunaan .stop
satu per satu. Bila konteks aplikasi
kita hanya berupa IO
kita dapat mengimplementasikan logika ini pada implementasi
Machines,IO]
. Namun, keuntungan menggunakan Free
adalah kita tidak harus
menyentuk kode yang sudah ada, namun kita hanya perlu mengisolasi dan mengetes
perilaku (sementara) ini tanpa harus terikat pada implementasi IO
.
7.5.2 FreeAp
(Applicative
)
Walaupun bab ini berjudul *Monad Lanjutan, poin utama adalah: kita tidak boleh
menggunakan monad kecuali bila kita memang benar benar harus. Pada bagian
ini, kita akan tahu mengapa FreeAp
(aplikatif free) lebih disukai dibandingkan
monad Free
.
FreeAp
didefinisikan sebagai representasi struktur data dari metoda ap
dan pure
dari kelas tipe Applicative
:
Metoda .hoist
dan .foldMap
seperti analog mereka dari Free
, .mapSuspension
dan .foldMap
.
Agar lebih mudah, kita dapat membuat Free[S, A]
dari FreeAp[S, A]
yang sudah
kita punyai dengan menggunakan metoda .monadic
. Pembuatan ini sangat berguna
terutama saat kita mengoptimasi subsistem Applicative
yang belum digunakan
sebagai bagian dari program Free
yang lebih besar.
Sebagaimana Free
, kita harus membuat FreeAp
untuk PSA kita. Hal ini juga
berarti kita harus membuat plat cetak lagi…
7.5.2.1 Pengelompokan Panggilan Jaringan
Kita akan membuka bab ini dengan klaim luar biasa mengenai performa. Saatnya membuktikannya.
Versi manusiawi dari angka latensi dari Peter Norvig yang ditulis oleh Philip Stark akan menjadi motivasi mengapa kita harus fokus untuk mengurangi panggilan melalui jaringan untuk mengoptimasi sebuah aplikasi:
Komputer | Skala Waktu Manusia | Analogi Manusia |
---|---|---|
Perujukan tembolok L1 | 0.5 detik | Satu detak jantung |
Salah prediksi cabang | 5 detik | Satu kali menguap |
Perujukan tembolok L2 | 7 detik | Satu kali menguap panjang |
Buka / tutup mutex | 25 detik | Buat satu cangkir teh |
Perujukan memori utama | 100 detik | Gosok gigi |
Kompresi 1Kb dengan Zippy | 50 menit | Satu putaran CI kompilator scala |
Kirim 2Kb melalu jaringan 1Gbps | 5.5 jam | Kereta London ke Edinburg |
Baca acak SSD | 1.7 hari | Akhir pekan |
Baca 1MB berurutan dari memori | 2.9 hari | Akhir pekan panjang |
Mengelilingi pusat data yang sama | 5.8 hari | Liburan panjang AS |
Baca 1MB berurutan dari SSD | 11.6 hari | Liburan pendek UE |
Pencarian di diska | 16.5 minggu | Satu semester kampus |
Baca 1MB berurutan dari diska | 7.8 bulan | Cuti melahirkan di Norwegia |
Kirim paket CA->Belanda->CA | 4.8 tahun | Satu periode pemerintahan |
Walaupun Free
dan FreeAp
memberikan beban memori tambahan, ekuivalen dari
100 detik untuk manusia, tiap kali kita memanggil dua panggilan berurutan di
sebuah kelompok panggilan, kita bisa menghemat 5 tahun.
Saat kita berada pada konteks Applicative
, kita dapat mengoptimasi aplikasi
kita dengan aman, tanpa harus menggagalkan ekspektasi apapun dari program asli.
Terlebih lagi, bisa menghindari pengaburan logika bisnis.
Untungnya, logika bisnis utamakita hanya meminta sebuah Applicative
. Harap
diingat
Kita akan mengawali dengan membuat plat cetak lift
untuk aljabar Batch
baru
dan kita akan membuat sebuah instans DynAgentsModule
dengan FreeAp
sebagai
konteks
Pada bab 6, kita telah mempelajari tipe data Const
yang memperkenankan kita
untuk menganalisis sebuah program. Tidak mengherankan bahwa FreeAp.analyze
diimplementasikan menggunakan Const
:
Kita menyediakan sebuah transformasi natural untuk mencatat semua pemulaian
simpul dan meng-.analyze
-is program kita untuk mendapatkan semua simpul
yang harus dijalankan:
Langkah selanjutnya adalah memperluas set instruksi dari Orig
menjadi Extended
yang juga mengikutsertakan Batch.Ast
dan menulis sebuah program FreeAp
yang
memulai semua simpul yang sudah dikumpulkan menggunakan metoda gathered
dalam
satu panggilan jaringan
Kita juga harus menghapus semua panggilan ke Machise.Start
yang dapat kita
lakukan dengan transformasi natural
Saat ini, kita mempunyai dua program dan harus menggabungkan keduanya. Harap
diingat bahwa sintaks *>
dari Apply
Dan menggabungkannya dalam sebuah metoda:
Demikian! Kita meng-.optimise
tiap kali kita memanggil act
pada ikalan utama
kita yang hanya berupa pekerjaan pertukangan.
7.5.3 Coyoneda
(Functor
)
Dinamai menggunakan nama dari matematikawan Nobuo Yoneda, kita dapat membuat
sebuah struktur data Functor
untuk semua aljabar S[_]
dan juga ada versi kontravariannya
API dari koyo cenderung lebih sederhana dari Free
dan FreeAp
, dan memperkenankan
sebuah transformasi natural dengan .trans
dan .run
(yang menerima sebuah
Functor
atau Contravariant
) untuk lepas dari struktur free.
Koyo dan kokoyo berguna bila kita ingin menggunakan .map
atau .contramap
kepada sebuah tipe dan kita tahu bahwa kita bisa mengkonversi menjadi sebuah
tipe data yang mempunyai instans Functor
, namun kita tidak mau benar-benar
melakukannya terlalu dini. Sebagai contoh, kita membuat sebuah Coyoneda[ISet, ?]
(harap diingat bahwa ISet
tidak mempunyai instans Functor
) untuk menggunakan
metoda lain yang membutuhkan sebuah Functor
, lalu mengkonversinya menjadi sebuah
List
di lain waktu.
Sebuah optimasi yang kita dapatkan dengan menggunakan Coyoneda
adalah
map fusion (dan contramap fusion), yang memperkenankan kita untuk menulis
ulang
menjadi
sehingga menghindari representasi sementara. Sebagai contoh, bila xs
merupakan
sebuah List
dengan seribu elemen, kita dapat menghemat dua ribu alokasi objek
karena kita hanya memetakan struktur data satu kali.
Namun, bisa dibilang jauh lebih mudah bila kita membuat perubahan semacam ini
pada fungsi awal secara manual atau menunggu proyek scalaz-plugin
dirilis dan secara otomatis melakukan optimasi semacam ini.
7.5.4 Efek Elastis
Program sebenarnya hanya data saja: struktur bebas membantu memperjelas hal ini dan memberikan kita kemampuan untuk mengatur ulang dan mengoptimasi data tersebut.
Free
lebih istimewa daripada yang terlihat: struktur ini dapat mengurutkan
aljabar dan kelas tipe secara arbiter.
Sebagai contoh, sebuah struktur free
untuk MonadState
tersedia. Ast
dan
.liftF
lebih rumit daripada biasanya karena kita harus memperhitungkan
parameter tipe S
pada MonadState
dan pewarisan dari Monad
:
Hal ini merupakan kesempatan yang bisa digunakan untuk mengoptimasi interpreter.
Sebagai contoh, kita dapat menyimpan S
pada bidang atomik, bukan pada trampolin
StateT
berlapis.
Kita dapat membuat sebuah Ast
dan .liftF
untuk hampir semua aljabar ataupun
kelas tipe. Satu-satunya batasan adalah F[_]
tidak muncul sebagai parameter
untuk instruksi apapun, misal, harus dimungkinkan agar aljabar mempunyai instans
Functor
. Sayangnya, hal ini menghapus kemungkinan MonadError
dan Monoid
.
Sebagaimana dengan PSA dari sebuah program free berkembang, performa mengalami
penurunan karena interpreter harus menyocokkan kepada set instruksi dengan biaya
O(n)
. Alternatif dari scalaz.Coproduct
adalah penyandian iotaz
yang menggunakan struktur data teroptimasi agar dapat bekerja pada O(1)
dengan
pelepasan dinamis yang menggunakan integer untuk tiap koproduk yang ditetapkan
pada saat kompilasi.
Untuk alasan sejarah, sebuah PSA free untuk sebuah aljabar atau kelas tipe
disebut Penyandian Awal. Dan, implementasi langsung (misal, dengan IO
) disebut
Akhirnya Kosong. Walau kita telah menjelajahi ide ide menarik dengan Free
,
secara umum diterima bahwa tanpa-label lebih unggul. Namun, untuk menggunakan
gaya akhirnya kosong (tanpa label), kita membutuhkan tipe efek dengan performa
tinggi yang menyediakan semua kelas tipe monad yang kita bahas pada bab ini. Kita
juga harus mampu menjalankan kode Applicative
kita secara paralel.
Persyaratan semacam ini akan kita bahas selanjutnya
7.6 Parallel
Ada dua operasi dengan efek yang hampir selalu kita jalankan secara paralel:
-
.map
atas sebuah koleksi dengan efek, mengembalikan sebuah efek. Hal ini dapat dicapai dengan.traverse
yang mendelegasikannya ke.apply2
milik sistem efek tadi. - menjalankan beberapa efek dengan jumlah tetap dengan operator jerit
|@|
, dan menggabungkan input efek-efek tadi, dan pada akhirnya mendelegasikan ke.apply2
.
Namun, praktik di lapangan, kedua operasi tersebut tidak dijalankan secara paralel
secara default. Alasannya adalah, bila F[_]
diimplementasikan dengan sebuah
Monad, maka hukum kombinator turunan untuk
.apply2` harus dipenuhi, yang berisi
Dengan kata lain, Monad
dilarang menjalankan efek secara paralel.
Namun, bila kita mempunyai sebuah F[_]
yang tidak bersifat monadik, maka konteks
ini bisa saja mengimplementasikan .apply2
secara paralel. Kita bisa menggunakan
@@
mekanisme untuk membuat sebuah instans dari Applicative
untuk F[_] @@ Paralel
,
yang mempermudah menentukan instans ke alias tipe Applicative.Par
Program monadik dapat meminta Par
implisit sebagai tambahan pada Monad
mereka
Sintaks Traverse
dari Scalaz mendukung paralelisme:
Bila Applicative.Par[IO]
ada pada cakupan secara implisit, kita dapat memilih
pelangkahan secara berurutan maupun paralel:
Tidak berbeda jauh, kita dapat memanggil .parApply
atau .parTupled
setelah
menggunakan operator jerit
Harap diperhatikan bahwa saat kita mempunyai program Applicative
, seperti
kita dapat menggunakan F[A] @@ Parallel
sebagai konteks dari program kita dan
kita mendapatkan paralelisme sebagai perilaku bawaan untuk .traverse
dan |@|
.
Konversi antara operasi mentah dan @@ Paralel
dari F[_]
harus ditangani secara
manual pada kode bantuan yang bisa melelahkan. Sehingga, akan lebih mudah bila
langsung meminta bentuk Applicative
7.6.1 Melanggar Hukum
Kita dapat mengambil pendekatan yang lebih berani terhadap paralelisme: dengan
tidak menaati hukum yang menyatakan bahwa .apply2
harus berurutan untuk Monad
.
Pendekatan ini sangat kontroversial, namun bekerja dengan sangat baik untuk
kebanyakan aplikasi di dunia nyata. Pertama, kita harus mengaudit basis kode
kita (termasuk ketergantungan pihak ketiga) untuk memastikan bahwa tidak ada
yang menggunakan hukum dari .apply2
.
Kita bungkus IO
dan sediakan implementasi buatan kita sendiri untuk Monad
yang menjalakan
.apply
secara paralel dengan mendelegasikan ke sebuah instans @@ Parallel
Sekarang kita bisa menggunakan MyIO
sebagai konteks aplikasi kita sebagai
pengganti IO
dan mendapatkan implementasi paralelisme secara default.
Agar lebih lengkap: sebuah implementsai naif dan tidak efisien dari Applicative.Par
untuk IO
sederhana kita dapat menggunakan Future
:
dan karena sebuah kutu pada kompilator
Scala yang memperlakukan semua instans @@
sebagai objek yatim, kita harus
secara tersurat mengimpor yang tersirat:
Pada bagian akhir bab ini, kita akan melihat bagaimana IO
Scalaz diimplementasikan
sebenar-benarnya.
7.7 IO
IO
Scalaz merupakan konstruk pemrograman asinkronus yang paling cepat pada
ekosistem Scala: hampir 50 kali lebih cepat bila dibandingkan dengan Future
.
IO
merupakan struktur data free yang khusus digunakan sebagai monad efek
umum.
IO
mempunyai dua parameter tipe: IO
memiliki Bifunctor
yang memperkenankan
tipe galat agar menjadi ADT spesifik aplikasi. Namun, karena kita berada pada JVM,
dan harus berinteraksi dengan pusaka warisan, sebuah tipe bantuan disediakan
agar dapat menggunakan tipe galat dari pengecualian:
7.7.1 Pembuatan
Ada berapa cara untuk membuat IO
yang meliputi varian blok kode lugas, lundung,
aman, dan tidak aman:
dengan konstruktor pembantu Task
:
Konstruktor yang paling jamak ditemui saat berurusan dengan kode warisan, sampai
saat ini, adalah Task.apply
dan Task.fromFuture
:
Kita tidak dapat mengumpankan Future
mentah dengan leluasa karena struktur data
ini dievaluasi secara tegas. Sehingga, kita harus selalu dibuat dalam blok yang
aman.
Harap diperhatikan bahwa ExecutionContext
tidak implicit
. Dan juga harap
diingat bahwa kita mencadangkan kata kunci implicit
untuk penurunan kelas tipe
untuk menyederhanakan bahasa: ExecutionContext
merupakan konfigurasi yang harus
disediakan secara tersurat.
7.7.2 Menjalankan
Interpreter IO
disebut sebagai RTS
, dari runtime system (sistem waktu-jalan).
Imlementasi interpreter ini diluar cakupan buku ini, kita akan fokus pada
fitur yang disediakan oleh IO
.
IO
hanya merupakan struktur data dan diinterpretasikan pada akhir waktu
dengan mengeksten SafeApp
dan menerapkan .run
Bila kita mengintegrasikan dengan sebuah sistem warisan dan tidak berkuasa
atas titik awal aplikasi kita, kita dapat mengeksten RTS
dan mendapatkan akses
pada metoda tak-aman untuk mengevaluasi IO
pada titik awal agar dapat mengacu
ke kode kita yang berprinsip pada pemrograman fungsional.
7.7.3 Fitur
IO
menyediakan instans kelas tipe untuk Bifunctor
, MonadError[E, ?]
, BindRec
,
Plus
, MonadPlus
(bila E
membentuk sebuah Monoid
), dan Applicative[IO.Par[E, ?]]
.
Sebagai tambahan atas fungsionalitas dari kelas tipe, ada beberapa implementasi metoda-metoda spesifik:
Adalah hal yang memungkinkan bila sebuah IO
berada pada kondisi terminated
yang merepresentasikan tugas yang dimaksudkan untuk dibuang (bukan galat maupun
sukses). Perkakas yang berhubungan dengan terminasi adalah:
7.7.4 Fiber
Sebuah IO
bisa saja membuat fiber, abstraksi ringan atas Thread
JVM.
Kita dapat melakukan .fork
kepada sebuah IO
dan melakukan pengawasan
(.supervise
) terhadap semua fiber yang belum lengkap untuk memastikan bahwa
fiber tersebut akan di-terminasi saat tindakan atas IO
selesai
Saat kita mempunyai sebuah Fiber
, kita dapat menggabungkannya kembali ke IO
dengan .join
, atau juga menghentikan dengan menggunakan interrupt
.
Kita dapat menggunakan fiber untuk mencapai bentuk kontrol konkuren optimistis.
Anggap sebuah situasi dimana kita mempunyai data
yang harus kita analis namun
kita juga harus memvalidasinya. Kita dapat secara optimistis memulai analisis
dan membatalkan tugas bila gagal divalidasi. Dan semua ini dilakukan secara
paralel.
Contoh penggunaan fiber lain adalah saat kita harus melakukan aksi tembak dan lupakan. Sebagai conoth, pencatatan log prioritas rendah melalui jaringan.
7.7.5 Promise
Sebuah promise merepresentasikan variable asinkronus yang dapat diatur tepat
satu kali (dengan complete
atau error
). Pendengar yang bisa mendapatkan nilai
variabel dengan get
tidak dibatasi.
Secara umum, kita jaran menggunakan Promise
pada kode aplikasi. Promise
merupakan blok bangun untuk framework konkurensi tingkat tinggi.
7.7.6 IORef
IORef
merupakan ekuivalen dari IO
untuk variabel atomik tidak tetap.
Kita dapat membaca variabel tersebut dan memiliki beberapa car untuk menulis atau memutakhirkannya.
IORef
merupakan blok bangun lain yang dapat digunakan untuk menyediakan MonadState
dengan performa tinggi. Sebagai contoh, buat sebuah newtype terspesialisasi
untuk Task
Kita dapat menggunakan implementasi teroptimasi StateMonad
ini pada sebuah
SafeApp
dimana .program
kita bergantung pada kelas tipe Pustaka Transformator
Monad:
Sebuah aplikasi yang realistis akan menerima beberapa aljabar dan kelas tipe sebagai masukan.
7.7.6.1 MonadIO
MonadIO
yang kita pelajari sebelumnya telah disederhanakan untuk menyembunyikan
parameter E
. Kelas tipe yang sebenarnya adalah
dengan perubahan kecil di plat cetak pada pendamping aljabar kita, untuk
mengikutsertakan tambahan E
:
7.8 Kesimpulan
-
Future
cacat, jangan digunakan. - Mengatur keamanan susunan memori dengan
Trampoline
. - Pustaka Transformator Monad (PTM) mengabstraksi efek-efek umum dengan kelas tipe.
- Transformator monad menyediakan implementasi default dari PTM.
- Struktur data
Free
memperkenankan kita untuk menganalisis, mengoptimasi, dan mengetes program kita. -
IO
memberi kita jalan untuk mengimplementasi aljabar sebagai efek dari dunia luar. -
IO
dapat menjalankan efek secara paralel dan merupakan tulang punggung dari aplikasi dengan performa tinggi.
8. Derivasi Kelas Tipe
Kelas tipe menyediakan fungsionalitas polimorfis untuk aplikasi kita. Namun, untuk menggunakan sebuah kelas tipe, kita butuh instans kelas tipe tersebut untuk objek domain bisnis kita.
Pembuatan instans kelas tipe dari instans yang sudah ada dikenal dengan derivasi kelas tipe dan menjadi topik pada bab ini.
Ada empat pendekatan atas derivasi kelas tipe:
- Instans manual untuk tiap objek domain. Pendekatan ini tidak mungkin dilakukan
pada aplikasi nyata karena akan menghasilkan ratusan baris plat cetak untuk
tiap baris
case class
. Namun, pendekatan ini berguna untuk tujuan pembelajaran dan optimasi performa. - Abstrak atas kelas tipe dari kelas tipe Scalaz yang sudah ada. Merupakan pendekatan
yang digunakan oloh
scalaz-deriving
, menyediakan tes terotomatisasi dan derivasi atas produk dan ko-produk. - Makro. Namun, penulisan makro untuk tiap kelas tipe harus dilakukan oleh pengembang yang sangat berpengalaman. Untungnya, pustaka Magnolia yang ditulis oleh Jon Pretty, mengabstraksi makro dengan APA yang sederhana dan memusatkan interaksi kompleks kepada kompilator.
- Menulis program generik dengan menggunakan pustaka Shapeless.
Mekanisme
implicit
merupakan sub-bahasa pada bahasa Scala dan dapat digunakan untuk menulis program pada tingkat tipe.
Pada bab ini, kita akan mempelajari kelas tipe yang semakin rumit dan derivasinya.
Kita akan memulai dengan scalaz-deriving
sebagai mekanisme paling sesuai dengan
prinsip, mengulangi beberapa pelajaran pada bab 5 mengenai Kelas Tipe Scalaz,
dan Magnolia (paling mudah digunakan), dan diakhiri dengan Shapeless (paling
leluasa) untuk kelas tipe dengan logika derivasi kompleks.
8.1 Contoh Berfungsi
Bab ini akan menunjukkan bagaimana cara mendefinisikan derivasi dari lima kelas tipe spesifik. Tiap contoh menunjukkan fitur yang dapat digeneralisasi:
8.2 scalaz-deriving
Pustaka scalaz-deriving
merupakan perpanjangan dari Scalaz dan dapat ditambahkan
ke build.sbt
proyek dengan
menyediakan kelas tipe baru, yang ditunjukkan dibawah, yang berhubungan dengan kelas tipe Scalaz
Sebelum kita memulai, berikut merupakan rekap ulang dari kelas tipe utama Scalaz:
8.2.1 Jangan Mengulang-Ulang
Cara paling sederhana untuk menderivasi sebuah kelas tipe adalah menggunakan ulang derivasi yang sudah ada.
Kelas tipe Equal
mempunyai instans Contravariant[Equal]
yang menyediakan
.contramap
:
Sebagai pengguna dari Equal
, kita dapat menggunakan .contramap
untuk tipe
data parameter tunggal kita. Harap diingat bahwa instans kelas tipe masuk pada
pendamping tipe data agar masuk pada cakupan implisit mereka:
Namun, tidak semua kelas tipe mempunyai instans Contravariant
. Terlebih lagi,
kelas tipe dengan parameter tipe pada posisi kovarian mungkin malah memiliki
instans Functor
:
Kita dapat menderivasi sebuah Default[Foo]
Bila sebuah kelas tipe mempunyai parameter pada posisi kovarian dan kontravarian,
seperti halnya Semigroup
, kelas tipe ini mungkin menyediakan sebuah instans
IntravariantFunctor
dan kita akan memanggil .xmap
Secara umum, jauh lebih mudah untuk menggunakan .xmap
bila dibandingkan dengan
menggunakan .map
atau .contramap
:
8.2.2 MonadError
Biasanya, sesuatu yang menulis dari sebuah nilai polimorfis mempunyai sebuah
Contravariant
. Dan, sesuatu yang membaca ke sebuah nilai polimorfis mempunyai
sebuah Functor
. Namun, sangat wajar bila pembacaan dapat gagal. Sebagai contoh,
bila kita mempunyai sebuah String
default, bukan berarti kita tinggal menurunkan
String Refined NonEmpty
darinya
yang gagal dikompilasi dengan galat
Mohon diingat bahwa pada bab 4.1, refineV
mengembalikan sebuah Either
,
sesuai dengan apa yang telah kompilator peringatkan.
Sebaga penulis dari kelas tipe Default
, kita dapat berbuat lebih daripada
Functor
dan menyediakan sebuah MonadError[Default, String]
:
Setelah mendapatkan akses ke sintaks .emap
dan dapat menderivasi tipe refined
Nyatanya, kita dapat menyediakan aturan derivasi untuk semua tipe terrefinasi
dimana Validate
berasal dari pustaka refined
dan dibutuhkan oleh refineV
.
Kita juga dapat menggunakan .emap
untuk menderivasi sebuah pembaca sandi
Int
dari sebuah Long
dengan perlindungan atas metoda non-total .toInt
dari pustaka standar.
Sebagai penulis dari kelas tipe Default
, kita mungkin ingin mempertimbangkan
ulang desain APA kita sehingga tidak akan gagal, misalkan dengan menggunakan
penanda tipe berikut
Kita tidak akan dapat mendefinisikan sebuah MonadError
, sehingga kita terpaksa
untuk menyediakan instans yang selalu sukses. Hal ini akan menghasilkan plat cetak
yang lebih banyak sebagai ganti atas keamanan tipe. Namun, kita akan tetap
menggunakan String \/ A
sebagai nilai kembalian karena ini merupakan contoh
yang lebih umum.
8.2.3 .fromIso
Semua kelas tipe di Scalaz mempunyai sebuah metoda pada objek pendampingnya dengan sebuah penanda yang mirip sebagai berikut:
Potongan diatas berarti bila kita mempunyai sebuah tipe F
dan sebuah cara untuk
mengkonversinya menjadi sebuah G
yang mempunyai sebuah instans, kita dapat
memanggil Equal.fromIso
untuk mendapatkan instans dari F
.
Sebagai contoh, sebagai pengguna kelas tipe, bila kita mempunyai tipe data Bar
,
kita dapat mendefinisikan sebuah isomorfisme ke (String, Int)
dan menderivasi Equal[Bar]
karena sudah ada Equal
untuk semua tuple:
Mekanisme .fromIso
juga dapat membantu kita sebagai penulis kelas tipe.
Sebagai contoh, Default
yang mempunyai penanda tipe utama dengan bentuk Unit => F[A]
.
Metoda default
kita sebenarnya isomorfik terhadap Kleisli[F, Unit, A]
,
atau transformator monad ReaderT
.
Karena Kleisli
sudah menyediakan sebuah MonadError
(bila F
sudah mempunyainya),
kita dapat menderivasi MonadError[Default, String]
dengan membuat sebuah
isomorfisme antara Default
dan Kleisli
:
memberikan kita .map
, .xmap
, dan .emap
yang sudah kita gunakan selama ini.
8.2.4 Divisible
dan Applicative
Untuk menderivasi Equal
pada kelas dengan dua parameter kita, kita akan
menggunakan ulang instans yang disediakan oleh Scalaz untuk tuple. Namun, dari
mana instans tuple itu berasal?
Kelas tipe yang lebih spesifik untuk Contravariant
adalah Divisible
. Equal
mempunyai sebuah instans:
Dan dari divide2
, Divisible
mampu membangun derivasi sampai ke divide22
.
Kita dapat memanggil metoda ini langsung ke tipe data kita:
Ekuivalen untuk parameter tipe ini pada posisi kovarian adalah Applicative
:
Namun, kita harus berhati hati agar kita tidak melanggar hukum kelas tipe
saat kita mengimplementasikan Divisible
atau Applicative
. Terlebih lagi,
sangat mudah untuk melanggar hukum komposisi yang menyatakan bahwwa kedua
alur-kode ini harus menghasilkan keluaran yang sama
divide2(divide2(a1, a2)(dupe), a3)(dupe)
divide2(a1, divide2(a2, a3)(dupe))(dupe)
- untuk semua
dupe: A => (A, A)
dengan hukum yang sama untuk Applicative
.
Misalk, JsEncoder
dan instans Divisible
yang diajukan
Pada satu sisi dari hukum komposisi, untuk sebuah input String
, kita akan
mendapatkan
dan pada sisi lain
yang berbeda. Kita dapat bereksperimen dengan implementasi divide
, namun
tidak akan pernah memenuhi hukum komposisi untuk semua input.
Hal ini mengakibatkan kita tidak dapat menyediakan sebuah Divisible[JsEncoder]
karena akan melanggar hukum matematika dan membatalkan semua asumsi yang digunakan
oleh pengguna
Divisible`.
Untuk membantu mengetes hukum, kelas tipe Scalaz berisi versi terkodifikasi dari hukum hukum atas kelas tipe itu sendiri. Kita dapat menulis tes terotomatis, memastikan bahwa hukum tersebut terlanggar, dan mengingatkan kita bahwa:
Di sisi lain, sebuah tes JsDecoder
memenuhi huku komposisi Applicative
untuk beberapa data tes
Sekarang, kita cukup yakin bathwa MonadError
yang telah kita derivasi memenuhi
hukum hukum yang berlaku.
Namun, bukan berarti bila kita lulus tes untuk set data kecil, hukum tidak terpenuhi. Kita harus menalar implementasi sampai tuntas agar kita yakin bahwa implementasi ini seharusnya sudah memenuhi hukum yang berlaku, dan mencoba permasalahan di luar batas normal yang bisa saja gagal.
Salah satu cara untuk menghasilkan data tes yang bervariasi adalah dengan menggunakan
pustaka scalacheck yang menyediakan
kelas tipe Arbitrary
yang dapat terintegrasi ke kebanyakan kerangka testing
untuk mengulang sebuah test dengan data yang dihasilkan secara acak.
Pustaka jsonformat
menyediakan sebuah Arbitrary[JsValue]
(dan semua orang
harus menyediakan Arbitrary
pada DTA mereka!) memperkenankan kita untuk menggunakan
fitur forall
dari Scalatest:
Tes ini memberikan kita lebih percaya pada kelas tipe kita memenuhi hukum komposisi
Applicative
. Dengan memeriksa seuma hukum pada Divisible
dan MonadError
kita juga mendapat banyak tes secara cuma-cuma.
8.2.5 Decidable
dan Alt
Bila Divisible
dan Applicative
memberikan kita derivasi kelas tipe untuk
produk (dibangun dari tuple) Decidable
dan Alt
memberikan kita ko-prooduk
yang dibangun dari disjungsi berlapis:
Empat kelas tipe utama mempunyai penanda simetris:
Typeclass | method | given | signature | returns |
---|---|---|---|---|
Applicative |
apply2 |
F[A1], F[A2] |
(A1, A2) => Z |
F[Z] |
Alt |
altly2 |
F[A1], F[A2] |
(A1 \/ A2) => Z |
F[Z] |
Divisible |
divide2 |
F[A1], F[A2] |
Z => (A1, A2) |
F[Z] |
Decidable |
choose2 |
F[A1], F[A2] |
Z => (A1 \/ A2) |
F[Z] |
mendukung kovarian produk, kovarian koproduk, kontravarian produk, dan kontravarian koproduk.
Kita dapat menulis sebuah instans Decidable[Equal]
yang memperkenankan kita
untuk menderivasi Equal
untuk semua TDA!
Untuk TDA
dimana produk (Vader
dan JarJar
) mempunyai instans Equal
kita dapat menderivasi persamaan untuk semua TDA
Kelas tipe yang mempunyai Applicative
berhak memiliki sebuah instans dari Alt
.
Bila kita ingin menggunakan trik Kleisli.iso
, kita dapat mengeksten IsomorphismMonadError
dan mencampurnya pada Alt
dan meningkatkan MonadError[Default, String]
agar
memililki Alt[Default]
:
Memperkenankan kita untuk menderivasi Default[Darth]
Kembali ke kelas tipe scalaz-deriving
, orangtua invarian dari Alt
dan Decidable
adalah:
mendukung kelas tipe dengan InvarianFunctor
seperti Monad
dan Semigroup
8.2.6 Arity Arbiter dan @deriving
Ada dua masalah dengan InvariantApplicative
dan InvariantAlt
:
- keduanya hanya mendukung produk dari 4 bidang dan koproduk dari 4 catatan.
- ada banyak plat cetak pada tipe data pendamping.
Pada bagian ini, kita akan menyelesaikan kedua permasalahan tersebut dengan kelas
tipe tambahan yang diperkenalkan oleh scalaz-deriving
Empat tipe kelas utama kita, Applicative
, Divisible
, Alt
, dan Decidable
,
diperluas menjadi arity arbiter menggunakan pustaka iotaz,
maka dari itu mendapatkan akhiran z
.
Pustaka iotaz mempunyai tiga tipe utama:
-
TList
which describes arbitrary length chains of types -
Prod[A <: TList]
for products -
Cop[A <: TList]
for coproducts -
TList
yang mendeskripsikan panjang rantai tipe arbiter -
Prod[A <: TList]
untuk produk -
Cop[A <: TList]
untuk koproduk
Sebagai contoh, sebuah representasi TList
dari Darth
pada bagian sebelumnya
adalah
yang dapat diinstansiasi:
Agar dapat menggunakan APA scalaz-deriving
, kita membutuhkan Isomorphism
antara TDA kita dengan representasi generik iotaz
. Akan sangat banyak plat cetak
yang terjadi:
Setelah menulis plat cetak diatas, kita dapat memanggil APA Deriving
untuk Equal
.
Hal ini mungkin terjadi karena scalaz-deriving
menyediakan instans teroptimasi
untuk Deriving[Equal]
Agar kelas tipe Default
dapat diperlakukan sama, kita harus menyediakan sebuah
instans Deriving[Default]
. Untuk hal ini, kita tinggal melapisi Alt
dengan
objek pembantu:
Kita telah menyelesaikan masalah arity arbiter, namun kita juga menambah plat cetak jauh lebih banyak.
Dan yang paling menjengkelkan, anotasi @deriving
yang disediakan oleh deriving-plugin
,
membuat semua plat cetak ini secara manual dan hanya perlu diterapkan pada bagian
atas sebuah TDA:
Yang juga diikut-sertakan pada scalaz-deriving
adalah instans dari Order
,
Semigroup
, dan Monoid
. Instans dari Show
dan Arbitrary
tersedia dengan
memasang scalaz-deriving-magnolia
dan scalaz-deriving-scalacheck
.
8.2.7 Contoh
Kita akan menutup pembelajaran kita mengenai scalaz-deriving
dengan implementasi
dari contoh kelas tipe yang bekerja seutuhnya. Sebelum kita melakukannya, kita
harus tahu tentang tipe data baru: /~\
yang juga dikenal dengan uler kasur,
yang berisi dua jenis struktur lebih tinggi yang berbagi tipe parameter yang sama:
Biasanya, kita menggunakan uler-kasur pada konteks Id /~\ TC
dimana TC
merupakan
kelas tipe, yang berarti kita mempunyai sebuah nilai dan sebuah instans dari
sebuah kelas tipe untuk nilai tersebut tanpa harus tahu apapun mengenai nilai
tadi.
Sebagai tambahan, semua metoda pada APA Deriving
mempunyai bukti tersirat
dengan bentuk A PairedWith FA
, memperkenankan pustaka iotaz
agar dapat melaksanakan
metoda .zip
, .traverse
, dan operasi lainnya pada Prod
dan Cop
. Kita dapat
mengabaikan parameter ini karena kita tidak menggunakannya secara langsung.
8.2.7.1 Equal
Sebagaimana dengan Default
, kita dapat mendefinisikan Decidable
biasa yang
memiliki arity tetap dan melapisinya dengan ExtendedInvariantAlt
(pendekatan
paling sederhana), namun kita memilih untuk mengimplementasikan Decidablez
secara langsung dengan alasa performa yang lebih baik. Kita juga menambah dua
optimasi tambahan:
- melakukan persamaan instans
.eq
sebelum menerapkanEqual.equal
, memperkenankan persamaan antar nilai-nilai identik. -
Foldable.all
memperkenankan untuk kelar awal saat hasil salah satu perbandingan bernilaifalse
. Misalkan, bila bidang pertama tidak cocok satu sama lain, maka kita perlu memeriksa persamaan pada bidang-bidang lainnya.
8.2.7.2 Default
Sayangnya, APA iotaz
untuk .traverse
(dan analognya, .coptraverse
) meminta
kita untuk mendefinisikan transformasi natural, yang mempunyai sintaks kikuk,
bahkan dengan tambahan kompilator kind-projector
.
8.2.7.3 Semigroup
Pendefinisian Semigroup
untuk koproduk umum tidak mungkin didefinisikan, namun
masih memungkinkan bila mendefinisikannya untuk produk umum. Kita dapat menggunakan
arity arbiter InvariantApplicative
:
8.2.7.4 JsEncoder
and JsDecoder
scalaz-deriving
tidak menyediakan akses ke nama bidang. Jadi tidak memungkinkan
untuk menulis penyandi dan pembaca sandi JSON.
8.3 Magnolia
Pustaka makro Magnolia menyediakan APA yang rapi untuk menulis derivasi kelas
tipe. Pemasangan Magnolia dapat dilakukan dengan menambah potongan berikut
pada build.sbt
Seorang penulis kelas tipe mengimplementasikan anggota-anggota berikut:
Sedangkan APA Magnolia:
dengan pembantu
Kelas tipe Monadic
, yang digunakan pada constructMonadic
, dibuat secara
otomatis bila tipe data kita mempunyai metoda .map
dan .flatMap
saat kita
mengimpor mercator._
.
Sebenarnya, tidak masuk akal bila kita menggunakan Magnolia untuk kelas tipe
yang dapat diabstraksi dengan Divisible
, Decidable
, Applicative
, atau Alt
karena abstraksi tersebut menyediakan struktur dan tes tambahan secara otomatis.
Namun, Magnolia menawarkan fitur yang tidak dapat diberikan oleh scalaz-deriving
:
akses ke nama bidang, nama tipe, anotasi, dan nilai default.
8.3.1 Contoh: JSON
Kita mempunyai beberapa pilihan desain mengenai serialisasi JSON yang harus dipilih:
- Haruskah kita mengikut-sertakan bidang dengan nilai
null
? - Haruskah pembacaan sandi memperlakukan nilai yang hilang dan
null
secara berbeda? - Bagaimana kita menyandikan nama dari sebuah koproduk?
- Bagaimana kita memperlakukan koproduk yang bukan berupa
JsObject
?
Kita akan memilih beberapa pengaturan default
- tidak mengikut sertakan bidang bila nilai bidang tersebut berupa
JsNull
. - menangani bidang yang hilang sama dengan nilai
null
. - menggunakan bidang khusus
"type"
untuk membedakan koproduk yang menggunakan nama tipe. - menempatkan nilai primitif pada bidang khusus
"xvalue"
.
dan memperkenankan pengguna untuk menambahkan anotasi ke bidang koproduk dan produk agar dapat mengubah format sesuai keinginan mereka:
Sebagai contoh
Dimulai dengan JsDecoder
yang hanya menangani pengaturan default kita:
Kita dapat melihat bagaimana APA Magnolia mempermudah pengaksesan nama bidang dan kelas tipe untuk tiap parameter.
Sekarang, kita akan menambah anotasi untuk menangani prarasa pengguna. Untuk menghindari mengingat-ingat anotasi pada tiap penyandian, kita akan menyimpannya pada tembolok dalam bentuk larik. Walaupun akses bidang pada sebuah larik tidak total, sebagai gantinya, kita mendapat jaminan bahwa indeks akan selalu selaras. Yang menjadi korban pada tarik-ulur antara spesialisasi dan generalisasi semacam ini adalah performa.
Untuk pembaca sandi, kita menggunakan .constructMonadic
yang mempunyai penanda
tipe mirip dengan .traverse
Hal yang sama, penambahan dukungan untuk prarasa pengguna dan nilai bidang default, dan juga bebarapa optimasi:
Kita memanggil metoda JsMagnoliaEncoder.gen
atau JsMagnoliaDecoder.gen
dari
objek pendamping tipe data kita. Sebagai contoh, APA Google Maps
Untungnya, anotasi @deriving
mendukung Magnolia. Bila penulis kelas tipe menyediakan
berkas deriving.conf
bersamaan dengan berkas jar mereka yang berisi teks berikut
deriving-macro
akan memanggil metoda yang disediakan oleh pengguna:
8.3.2 Derivasi Otomatis
Penghasilan instans implicit
pada objek pendamping tipe data, secara historis,
dikenal sebagai derivasi semi-otomatis. Berbeda dengan derivasi otomatis dimana
.gen
dibuat implisit
Penggguna dapat mengimpor metoda ini ke cakupan kode mereka dan mendapatkan derivasi otomatis pada saat penggunaan
Mungkin terlihat menggiurkan, karena pengguna tidak perlu repot menulis kode, namun ada dua kerugian penting:
- makro diselawat pada setiap penggunaan, misal tiap kali kita memanggil
.toJson
. Hal semacam ini memperlambat kompilasi dan juga menghasilkan objek lebih banyak pada saat waktu-jalan, yang secara tidak langsung berdampak pada performa waktu-jalan. - kemungkinan derivasi tak terduga.
Kerugian pertama cukup jelas, namun derivasi yang tak terduga akan terejawantah sebagai kutu yang hampir tidak kasat mata. Anggap contoh berikut
bila kita lupa menyediakan derivasi implisit untuk Option
. Mungkin kita berharap
Foo(Some("hello"))
akan menjadi
Namun yang muncul adalah
karena Magnolia menderivasikan penyandi Option
untuk kita.
Hal semacam ini sangat membingungkan. Kita lebih memilih agar kompilator memberi-tahu kita bila kita lupa sesuatu. Maka dari itu, penderivasian otomatis tidak direkomendasikan.
8.4 Shapeless
Pustaka Shapeles dikenal sebagai pustaka
paling rumit pada ekosistem Scala. Alasannya, pustaka ini menggunakan fitur bahasa
implicit
dengan sangat mendalam dengan membuat semacam bahasa pemrograman generik
pada tingkat tipe.
Hal semacam ini tidak sepenuhnya asing: pada Scalaz, kita membatasi penggunaan
fitur bahasa implicit
hanya pada kelas tipe. Namun, kadang kita meminta kompilator
menyediakan kita bukti yang behubungan dengan tipe. Sebagai contoh, hubungan
Liskov atau Leibniz (<~<
dan ===
) dan saat melakukan Inject
ke sebuah
aljabar scalaz.Coproduct
dengan sebuah aljabar free.
Untuk memasang Shapeless, tambahkan potongan kode berikut ke build.sbt
Inti dari Shapeless adalah tipedata
HList dan
Coproduct`
yang merupakan representasi generik dari produk dan koproduk, sedangkan
sealed trait HNil
digunakan sebagai pembantu agar kita tidak perlu menulis
HNil.type
Shapeless juga mempunyai salinan tipe data IsoSet
yang disebut sebagai Generic
yang memperkenankan kita untuk berpindah antara sebuah TDA dan representasi
generiknya:
Banyak dari tipe Shapeless mempunyai tipe anggot (Repr
) dan alias tipe .Aux
(bantuan, auxiliary) pada objek pendamping yang membuat tipe kedua muncul
terlihat. Hal ini memperkenankan kita untuk meminta Generic[Foo]
untuk tipe
Foo
tanpa harus menyediakan representasi generiknya karena sudah dibuat oleh
sebuah makro.
Ada juga komplementer LabelledGeneric
yang mengikutsertakan nama bidang
Harap diperhatikan bahwa nilai dari sebuah representasi LabelledGeneric
sama dengan representasi Generic
. Nama bidang hanya ada pada tipe dan dihapus
pada waktu-jalan.
Kita tidak perlu untuk menulis KeyTag
secara manual karena kita dapat menggunakan
tipe alias:
Bila kita ingin mengakses nama bidang dari sebuah FieldType[K, A]
, kita dapat
meminta bukti implisit Witness.Aux[K]
yang memperkenankan kita untuk mengakses
nilai dari K
pada waktu-jalan.
Secara sekilas, ini semua yang harus kita tahu mengenai Shapeless agar dapat menderivasi sebuah kelas tipe. Namun, karena semua hal semakin rumit, misalkan jawaban kapan kawin, punya anak, dan pensiun, kita akan melanjutkan pembahasan dengan contoh yang juga semakin kompleks.
8.4.1 Contoh: Equal
Pola yang umum digunakan adalah mengeksten kelas tipe yang ingin kita derivasi dan menempatkan kode Shapeless pada objek pendampingnya. Pola ini memberikan kita cakupan implisit yang dapat dicari oleh kompilator tanpa harus melakukan impor yang rumit.
Titik mulai dari derivasi Shapeless adalah metoda gen
yang meminta dua parameter
tipe: A
sebagai yang kita derivasikan dan R
sebagai representasi generiknya.
Lalu kita akan meminta Generic.Aux[A, R]
, menghubungkan A
ke R
, dan sebuah
instans dari kelas tipe Derived
untuk R
. Kita memulai dengan penanda dan
implementasi sederhana berikut:
Kita telah mereduksi permasalahan atas penyediaan sebuah Equal[R]
implisit
untuk R
yang merupakan representasi generik dari A
. Pertam, perhatikan produk
yang berupa R <: HList
. Penanda inilah yang kita inginkan untuk diimplementasikan:
karena bila kita dapat mengimplementasikannya untuk head dan tail, komplire
akan dapat mengulang metoda ini sampai pada akhir daftar. Hal ini membawa kita
pada keharusan untuk menyediakan sebuah instans untuk HNil
kosong
Kita akan mengimplementasikan metoda berikut
dan untuk kooproduk, kita ingin mengimplementasikan penanda berikut
.cnil
tidak akan pernah dipanggil untuk kelas tipe dengan parameter tipe yang
hanya ada pada posisi kontravarian, seperti Equal
, namun koompiler tidak tahu
mengenai hal tersebut. Jadi, kita akan menyediakan potongan kode berikut:
Untuk koproduk, kita hanya bisa membandingkan dua hal bila mereka selaras. Atau
keduanya Inl
atau Inr
Hal yang patut dicatat adalah metoda kita selaras dengan konsep conquer
(hnil
),
divide2
(hlist
), dan alt2
(coproduct
). Namun, kita tidak mendapat
keuntungan apapun seperti pengimplementasian Decidable
. Hal ini berarti kita
harus memulai dari awal bila kita menulis tes untuk kode ini.
Mari kita tes kode berikut dengan TDA sederhana
Kita harus menyediakan instans pada objek pendamping:
Namun, kode tersebut tidak dapat dikompilasi
Nah, galat kompilasi Shapeless terlihat seperti ini.
Masalah ini, yang sama sekali tidak jelas terlihat dari pesan galat, terjadi
karena kompilator tidak dapat menentukan R
dan mengira R
sebagai tipe lainnya.
Kita harus menyediakan parameter tipe eksplisit saat memanggil gen
, mis.
atau kita dapat menggunakan makro Generic
agar kompilator dapat menebak representasi
generiknya
Penanda tipe-lah yang menyelesaikan masalah tersebut
yang dijabarkan menjadi
Kompiler Scala menyelesaikan batasan tipe dari kiri ke kanan. Jadi kompilator
akan mencari banyak solusi untuk DerivedEqual[R]
sebelum membatasinya menjadi
Generic.Aux[A, R]
. Cara lain untuk menyelesaikan masalah ini adalah dengan
tidak menggunakan batasan konteks.
Berbekal pengetahuan ini, kita tidak perlu lagi implicit val generic
atau tipe
parameter eksplisit pada panggilan .gen
. Kita dapat menggunakan @deriving
dan menambahkan sebuah catatan pada deriving.conf
(dengan asumsi kita ingin
menimpa implementasi scalaz-deriving
)
dan menulis
Namun, mengganti versi scalaz-deriving
juga berarti waktu kompilasi akan semakin
panjang. Hal ini disebabkan karena kompilator menyelesaikan pencarian implisit N
untuk tiap produk bidang N
atau koproduk dari produk N
. Hal semacam ini
tidak terjadi pada scalaz-deriving
dan Magnolia.
Harap dicatat saat menggunakan scalaz-deriving
atau Magnolia, kita dapat
menuliskan @deriving
pada bagian atas anggota dari sebuah TDA. Shapeless
meminta perlakuan yang berbeda dengan mengharuskan kita untuk menambahkannya pada
semua bagian.
Namun, implementasi ini masih memiliki kutu: kegagalan pada tipe rekursif saat waktu jalan, mis.
Alasan hal ini terjadi adalah Equal[Tree]
bergantung pada Equal[Branch]
yang
bergantung pada Equal[Tree]
. Dan terjadilah rekursi. Maka dari itu, implementasi
ini harus dipanggil secara lantung.
scalaz-deriving
dan Magnolia secara otomatis melakukan evaluasi lantung dan
lagi-lagi Shapeless mengambil penekatan yang berbeda karena menyerahkan sepenuhnya
ke penulis kelas tipe.
Tipe makro Cached
, Strict
, dan Lazy
mengubah perilaku inferensi kompilator
dan memperkenankan kita untuk mendapatkan kelantungan yang kita butuhkan. Pola
yang harus diikut adalah dengan menggunakan Cached[Strict[_]]
pada titik masuk
dan Lazy[_]
pada instans H
.
Akah jauh lebih baik untuk tidak lagi menggunakan batasan konteks dan tipe SAM pada titik ini:
Sembari melepas batasan konteks, kita juga mengoptimasi dengan menggunakan
jalan pintas quick
dari scalaz-deriving
.
Sekarang, kita dapat memanggil
tanpa mendapatkan pengecualian waktu-jalan.
8.4.2 Contoh: Default
Tidak ada jebakan baru pada implementasi dari kelas tipe dengan parameter tipe
di posisi kovarian. Disini, kita membuat nilai HList
dan Coproduct
dan harus
menyediakan nilai untuk CNil
karena nilai ini berhubungan dengan permasalah
dimana tidak ada koproduk yang mampu menyediakan nilai tersebut.
Seperti analogi yang dapat kita tarik antara Equal
dan Decidable
, kita dapat
melihat hubungan antara Alt
pada .point
(hnil
), .apply2
(.hcons
),
dan .altly2
(.ccons
).
Tidak banyak yang bisa dipelajari dari contoh seperti Semigroup
, jadi kita
akan melewati penyandian dan pembacaan sandi.
8.4.3 Contoh: JsEncoder
Agar dapat mereproduksi penyandi JSON Magnolia kita, kita harus mampu mengakses
- nama bidang dan nama kelas
- anotasi untuk prarasa pengguna
- nilai default pada sebuah
case class
Kita akan memulai dengan membuat sebuah penyandi yang hanya menangani pengaturan yang masuk akal.
Untuk mendapatkan nama bidang, kita menggunakan LabelledGeneric
, bukan Generic
,
dan pada saat mendefiniskan tipe dari elemen awal, kita menggunakan FieldType[K, H]
,
bukan hanya H
. Sebuah Witness.Aux[K]
menyediakan nilai dari nama bidang pada
saat waktu-jalan.
Semua metooda kita akan mengembalikan JsObject
, bukan JsValue
, agar kita
dapat mengkhususkan dan membuat DerivedJsEncoder
yang mempunyai penanda tipe
berbeda dengan JsEncoder
.
Shapeless menentukan jalur kode pada saat kompilasi berdasarkan pada ada atau tidaknya anotasi yang dapat memberikan potensi kode teroptimasi, walaupun dengan beban repetisi kode. Hal semacam ini juga berarti bahwa jumlah anotasi yang kita urus, termasuk sub-tipenya, harus bisa diatur atau bisa saja kita menulis 10 kali jumlah kode. Kita dapat mengganti tiga anotasi kita menjadi satu anotasi yang berisi semua parameter kustomasi:
Semua pengguna anotasi harus menyediakan tiga nilai default dan metoda pembantu tidak tersedia untuk konstruktor anotasi. Kita dapat menulis pengekstrak kustom sehingga kita tidak harus mengganti kode Magnolia kita.
Kita dapat meminta Annotation[json, A]
untuk case class
atau sealed trait
agar mendapatkan akses ke anotasi. Namun, kita harus menulis hcons
dan ccons
untuk menangani kedua kasus tersebut karena bukti tidak akan dibuat bila anotasi
tidak ada. Maka dari itu, kita memperkenalkan cakupan implisit dengan prioritas
yang lebih rendah dan meletakkan bukti “tanpa anotasi” disana.
Kita juga dapat meminta bukti Annotations.Aux[json, A, J]
untuk mendapatkan
HList
dari anotasi json
untuk tipe A
. Hal yang sama, kita harus menyediakan
hcons
dan ccoons
untuk menangani kejadian ada-atau-tidaknya sebuah anotasi.
Untuk mendukung anotasi satu ini, kita harus menulis kode empat kali lebih banyak.
Dimulai dengan menulis ulang JsEncoder
yang hanya menangani kode pengguna yang
tidak mempunyai anotasi apapun. Sekarang, setiap kode yang menggunakan @json
akan gagal dikompilasi. Hal ini merupakan jaring pengaman yang cukup baik.
Kita harus menambah sebuah tipe A
dan J
ke DerivedJsEncoder
dan melangkahi
anotasi pada metoda .toJsObject
-nya. Bukti .hcons
dan .ccons
sudah menyediakan
instans untuk DerivedJsEncoder
dengan anotasi None.type
dan kita akan memindahkan
mereka ke prioritas yang lebih rendah sehingga kita dapat menangani Annotation[json, A]
di prioritas yang lebih tinggi.
Harap perhatikan bahwa bukti untuk J
sudah diberikan sebelum R
. Hal ini
sangat penting karena kompilator harus menyelesaikan J
sebelum dapat menyelesaikan
R
.
Sekarang kita dapat menambah penanda tipe untuk enam metoda baru tersebut, dan memenuhi semua kemungkinan dimana anotasi mungkin berada. Harap juga perhatikan bahwa kita hanya mendukung satu anotasi pada setiap posis. Bila pengguna menyediakan beberapa anotasi, semua anotasi lain setelah anotasi pertama akan diabaikan.
Saat ini, kita sudah kehabisan nama untuk banyak hal. Maka dari itu, kita akan
menyebutnya, secara arbiter, sebagai Annotated
bila A
sudah memiliki anotasi
dan Custom
bila ada sebuah anotasi pada sebuah bidang:
Kita tidak benar-benar butuh .hconsAnnotated
atau .hconsAnnotatedCustom
, karena
anotasi pada sebuah case class
tidak berarti apapun pada penyandian produk tersebut.
kedua metoda tersebut hanya dipakai pada .cconsAnnotated*
. Maka dari itu, kita
dapat menghapus kedua metoda tersebut.
.cconsAnnotated
dan .cconsAnnotatedCustom
dapat didefinisikan sebagai
dan
Guna .head
dan .get
mungkin meragukan, namun harap diingat bahwa tipe disini
berupa ::
dan Some
yang berarti metoda ini total dan aman digunakan.
.hconsCustom
dan .cconsCustom
ditulis
dan
Terang saja, ada sangat banyak plat cetak, namun bila diperhatikan lebih seksama, kita dapat melihat bahwa tiap metoda diimplementasika se-efisien mungkin dengan informasi yang tersedia: alur kode dipilih saaat waktu kompilasi, bukan pada saat waktu jalan.
Bagi para pengguna yang sangat menginginkan performa bisa saja melakukan faktorisasi
ulang pada kode ini, sehingga informasi anotasi tersedia lebih awal, bukan disisipkan
pada metoda .toJsFields
dengan lapisan lain. Untuk performa puncak, kita
dapat memperlakukan tiap kustomasi sebagai anotasi terpisah, namun hal tersebut
menambah lagi jumlah kode yang kita tulis, dengan tambahan beban waktu kompilasi
pada pengguna hilir. Optimasi semacam itu berapa diluar cakupan buku ini, namun
hal tersebut bisa saja dilakukan dan praktik di lapangan memang demikian adanya:
pemindahan beban kerja dari waktu-jalan ke waktu-kompilasi merupakan hal yang
paling menarik dari pemrograman generik.
Satu lagi kekurangan yang harus kita sadari: LabelledGeneric
tidak kompatibel dengan scalaz.@@
,
namun, tentu saja ada penyiasatannya. Misalkan kita ingin mengabaikan label, kita
dapat menambah aturan derivasi berikut pada objek pendamping dari penyandi dan
pembaca sandi
Lalu kita dapat men-derivasi sebuah JsDecoder
untuk TradeTemplate
kita dari
bab 5
Namun, kita malah mendapat sebuah galat kompilator
Penyiasatan masalah ini adalah dengan memperkenalkan bukti untuk H @@ Z
pada
cakupan impliist yang lebih rendah dan memanggil kode tersebut, sehingga kompilator
dapat menemukannya:
Dan untungnya, kita hanya perlu memikirkan tentang produk, karena koproduk tidak dapat dilabeli.
8.4.4 JsDecoder
Bagian pembacaan sandi kurang lebih sama dengan contoh sebelumnya. Kita dapat
menyusun sebuah instans dari Field[K, H]
dengan metoda bantuan field[K](h: D]
.
Kita hanya menulis default yang masuk akal saja:
Menambahkan prarasa pengguna dengan cara anotasi, sama halnya dengan DerivedJsonEncoder
dan terasa kaku. Jadi, kita akan memperlakukannya sebagai latihan bagi pembaca.
Satu hal penting yang tertinggal: nilai default case class
. Kita dapat meminta
bukti namun yang menjadi masalah adalah kita tidak dapat lagi menggunakan mekanisme
derivasi yang sama untuk produk dan koproduk: bukti tidak akan pernah dibuat untuk
koproduk.
Solusi yang dipakai cukup drastis, kita harus memisah DerivedJsDecoder
menjadi
DerivedCoproductJsDecoder
dan DerivedProductJsDecder
. Kita akan memfokuskan
perhatian kita pada DerivedProductJsDecoder
dan menggunakan Map
untuk pencarian
bidang yang lebih singkat:
Kita dapat meminta bukti nilai default dengan Default.Aux[A, D]
dan menduplikasi
semua metooda agar menangani kasus yang tergantung dari ketersediaan nilai default.
Namun, Shapeless berbaik-hati (untuk kali ini) dan menyediakan Default.AsOoptions.Aux[A, D]
yang memperkenankan kita untuk menangani nilai default pada saat waktu-jalan.
Kita harus memindahkan metoda .hcons
dan .hnil
ke objek pasangan dari kelas
tipe tertutup, yang dapat menangani nilai default
We can no longer use @deriving
for products and coproducts: there can only be
one entry in the deriving.conf
file.
Kita tidak dapat lagi menggunakan @deriving
untuk produk dan koproduk: hanya
boleh ada satu entry pada berkas deriving.conf
.
Dan jangan lupa untuk menambahkan dukungan @@
8.4.5 Derivasi Rumit
Shapeless memperkenankan lebih banyak jenis derivasi bila dibandingkan dengan
scalaz-deriving
atau Magnolia. Sebagai contoh, sebuah penyandi / pembaca sandi
yang tidak mungkin bisa dilakukan dengan Magnolia. Sebagai contoh, model XML dari
xmlformat
Dikarenakan sifat dari XML, akan masuk akal bila kita memiliki pasangan penyandi /
pembaca sandi untuk konten XChildren
dan XString
. Kita dapat menyediakan
sebuah derivasi untuk XChildren
dengan Shapeless, namun kita ingin sebuah
bidang khusus untuk jenis kelas tipe yang dimilikinya dan juga untuk bidang Option
.
Kita harus mewajibkan bidang-bidang dianotasi dengan nama tersandi. Sebagai
tambahan, saat membaca penyandian, akan lebih baik bila kita memiliki strategi
yang berbeda untuk menangani elemen dari XML, yang mungkin berupa banyak bagian,
tergantung bila tipe kita mempunyai Semigroup
, Monoid
, ataupun tidak sama sekali.
8.4.6 Contoh: UrlQueryWriter
Sama halnya dengan xmlformat
, aplikasi drone-dynamic-agents
dapat diuntungkan
dari derivasi kelas tipe dari kelas tipe UrqQueryWriter
yang dibangun dengan
instans UrlEncodedWriter
untuk tiap entry bidang. Kelas tipe ini tidak mendukung
koproduk:
Cukup masuk akal bila kita bertanya apakan 30 baris kode ini memang peningkatan dari 8 baris untuk 2 instans manual yang dibutuhkan oleh aplikasi kita: pilihan yang ditentukan kasus-per-kasus.
Agar lebih lengkap, derivasi UrlEncodedWriter
dapat ditulis dengan Magnolia
8.4.7 Sisi Gelap Derivasi
“Beware fully automatic derivation. Anger, fear, aggression; the dark side of the derivation are they. Easily they flow, quick to join you in a fight. If once you start down the dark path, forever will it dominate your compiler, consume you it will.”
― an ancient Shapeless master
Selain semua peringatan mengenai derivasi otomatis yang telah ditulis untuk Magnolia, Shapeless jauh lebih mengkuatirkan. Tak hanya derivasi otomatis Shapeless sering kali penyebab lambannya kompilasi, Shapeless juga merupakan sumber dari kutu koherensi kelas tipe.
Derivasi otomatis adalah ketika def gen
bersifat implicit
yang mana sebuah
panggilan akan melakukan rekursi untuk semua entry pada TDA. Karena cara kerja
cakupan implisit, sebuah implicit def
akan memiliki prioritas lebih tinggi
dibandingkan dengan instans kustom pada objek pendamping dan menjadi sumber
dekoherensi kelas tipe. Sebagai contoh, perhatikan kode berikut bila .gen
kita
implicit
Kita mungkin berharap bentuk tersandi otomatis dari Bar("hello")
terlihat
seperti
karena kita menggunakan xderiving
untuk Foo
. Namun, hasil yang diberikan
adalah sebagai berikut
Yang lebih parah adalah saat metoda implisit ditambahkan pada objek pendamping dari kelas tipe. Hal ini berarti kelas tipe selalu diderivasi pada saat penggunaan dan pengguna tidak dapat memilih untuk tidak menggunakannya.
Secara mendasar, saat menulis program generik, implicit
dapat diabaikan oleh
kompilator, bergantung pada cakupan. Hal ini berarti kita kehilangan keamanan pada
waktu-kompilasi yang menjadi motivasi utama atas pemrograman pada tingkat tipe.
Semua akan menjadi lebih mudah pada sisi baik, dimana implicit
hanya digunakan
untuk kelas tipe yang koheren dan unik secara global. Ketakutan atas plat cetak
merupakan pemicu ke sisi gelap. Ketakutan akan berubah menjadi amarah. Amarah
akan menyebabkan benci. Dan, benci akan menyebabkan penderitaan.
8.5 Performa
Tidak ada sesuatu yang namanya Busur Gandiwa bila kita berbicara mengenai derivasi kelas tipe. Salah satu hal yang harus dipertimbangkan adalah performa: baik pada saat kompilasi maupun waktu-jalan.
8.5.0.1 Waktu Kompilasi
Bila kita berbicara mengenai waktu kompilasi, Shapeless merupakan pencilan. Bukan
hal yang luar biasa bila kita mendapati sebuah proyek kecil menderita penggelembungan
waktu kompilasi dari satu detik menjadi satu menit. Untuk mengusut masalah kompilasi,
kita dapat melakukan profiling terhadap aplikasi kita dengan menggunakan
plugin scalac-profiling
Potongan diatas akan menghasilkan keluaran yang dapat menghasilkan sebuah graf api.
Untuk derivasi Shapeless yang jamak digunakan, kita akan mendapat grafik yang menarik
hampir semua waktu kompilasi dihabiskan untuk resolusi implicit
. Harap diperhatikan,
grafik ini juga mengikutsertakan kompilasi scalaz-deriving
, Magnolia, dan instans
manual. Namun, komputasi dari Shapeless mendominasi grafik tersebut.
Dan, ini bila kita berhasil melakukan profiling. Bila ada masalah dengan derivasi shapeless, kompilator dapat tersangkut pada sebuah ikalan tak hingga dan harus dibunuh.
8.5.0.2 Performa Waktu-Jalan
Bila kita berbicara mengenai performa waktu-jalan, tentu jawabannya selalu tergantung.
Bila kita mengasumsikan logika derivasi sudah ditulis secara efisien, kita dapat mengetahui mana yang lebih cepat dengan menggunakan eksperimentasi.
Pustaka jsonformat
menggunakan Java Microbenchmark Harness (JMH)
pada model yang memetakan ke GeoJSON, GoogleMaps, dan Twitter. Pustaka ini dikontribusikan
oleh Andriy Plokhotnyuk. Ada tiga tes untuk tiap model:
- penyandian TDA ke
JsValue
- pembacaan sandi yang berhasil dari
JsValue
dari poin pertama kembali ke TDA - pembacaan sandi yang gagal dari
JsValue
dengan data galat
diterapkan pada implementasi berikut:
- Magnolia
- Shapeless
- manual
dengan optimisasi yang setara untuk semua implementasi. Hasil berupa operasi per detik (lebih tinggi lebih baik), pada sebuah komputer desktop bertenaga, menggunakan satu utas:
Sebagaimana yang kita lihat, implementasi manual memimpin dan diikuti oleh Magnolia. Implementasi dengan Shapeless memiliki performa 30 - 70% lebih buruk bila dibandingkan dengan instans manual. Sekarang untuk pembacaan sandi
Pacuan kali ini lebih ketat untuk tempat kedua, dengan Shapeless dan Magnolia
menorehkan hasil yang mirip. Dan pada akhirnya, pembacaan sandi dari sebuah
JsValue
yang berisi data tidak valid (pada posisi yang memang disengaja agak
kikuk)
Saat kita mengira kita menemukan sebuah pola, Magnolia dan Shapeless menang pacuan tersebut saat membaca penyandian tidak valid dari data GeoJSON. Namun, instans manual memenangkan pacuan Google Maps dan Twitter.
Kita ingin mengikut-sertakan scalaz-deriving
pada perbandingan, jadi kita akan
membandingan implementasi setara dari Equal
yang dites pada dua nilai yang
berisi nilai yang sama (True
) dan dua nilai yang memiliki isi yang sedikit berbeda
(False
)
Sesuai dengan perkiraan kita, instans manual jauh lebih cepat bila dibandingkan
dengan yang lainnya. Disusul dengan Shapeless dengan derivasi otomatis. scalaz-deriving
bekerja keras untuk GeoJSON namun kalah mengenaskan pada pacuan Google Maps dan
Twitter. Tes tentang False
kurang lebih memiliki hasil yang sama:
Performa waktu-jalan dari scalaz-deriving
, Magnolia, dan Shapeless biasanya
cukup baik. Tentu kita juga harus realistis: kita tidak menulis aplikasi yang
harus mampu menyandikan 130.000 nilai ke JSON tiap detiknya, pada satu core,
di JVM. Bila hal semacam itu menjadi masalah, silakan berpaling ke C++.
Agak tidak mungkin instans terderivasi menjadi penyebab macetnya performa aplikasi. Bahkan bila memang demikian adanya, ada perahu penyelamat dengan penulisan ulang, yang jauh lebih leluasa dan berbahaya: lebih mudah terjadi salah ketik, pengenalan kutu, dan kemunduran performa tanpa sengaja saat menulis instans manual.
Kesimpulannya: derivasi tipu-tipu dan makro jaman baheula bukan tandingan untuk instans yang ditulis secara manual, tong.
8.6 Kesimpulan
Saat menentukan teknologi yang akan digunakan untuk menderivasi kelas tipe, bagan fitur ini mungkin membantu:
Fitur | Scalaz | Magnolia | Shapeless | Manual |
---|---|---|---|---|
@deriving |
ya | ya | ya | |
Hukum | ya | |||
Kompilasi copat | ya | ya | yes | |
Nama bidang | ya | ya | ||
Anotasi | ya | sebagian | ||
Nilai default | ya | dengan kurang | ||
Rumit | memedihkan | |||
Performa | masuk pak eko |
Pilih scalaz-deriving
bila memungkinkan, gunakan Magnolia untuk penyandian /
pembacaan sandi atau bila performa agak penting, dan gunakan Shapeless untuk
derivasi yang rumit bila waktu kompilasi tidak menjadi masalah.
Instans manual selalu menjadi pelampung untuk kasus khusus dan untuk mencapai performa paling akhir. Hindari kutu karena salah ketik pada instans manual dengan menggunakan alat penghasil kode.
9. Merangkai Aplikasi
Untuk menutup buku ini, kita akan menerapkan apa yang telah kita pelajari dengan menulis contoh aplikasi dan mengimplementasikan sebuah klien dan peladen HTTP menggunakan pustaka pemrogaram fungsional murni http4s.
Kode sumber dari aplikasi drone-dynamic-agents
tersedia bersama dengan sumber
kode buku pada https://github.com/fommil/fpmortals
pada direktori example
.
Untuk membaca bab ini, tidak perlu berada di depan komputer, namun kebanyakan
pembaca mungkin memilih untuk melihat-lihat basis-kode sebagai tambahan tulisan
ini.
Beberapa bagian dari aplikasi sengaja belum diimplementasikan dan digunakan
sebagai latihan bagi pembaca. Silakan lihat README
untuk instruksi lebih lanjut.
9.1 Ikhtisar
Aplikasi utama kita hanya membutuhkan sebuah implementasi untuk aljabar DynAgents
.
Kita sudah memiliki sebuah implementasi, DynAgentsModule
, yang membutuhkan
implementasi dari aljabar Drone
dan Machines
, yang juga membutuhkan sebuah
aljabar JsonClient
, LocalClock
, OAuth2, dan lain lain.
Gambaran utuh dari semua aljabar, modul, dan interpreter sangat berguna. Berikut tata letak kode sumber:
Penanda dari semua aljabar dapat diikhtisarkan sebagai
Harap diperhatikan bahwa beberapa penanda dari bab sebelumnya sudah difaktorisasi ulang agar menggunakan tipe data Scalaz karena kita tahu bahwa tipe data tersebut lebih unggul bila dibandingkan dengan pustaka standar.
Tipe data tersebut adalah:
dan kelas tipe yang digunakan adalah:
Kita menderivasi kelas tipe yang berguna menggunakan scalaz-deriving
dan Magnolia.
Kelas tipe ConfigReader
berasal dari pustaka pureconfig
dan digunakan untuk
membaca konfigurasi waktu-jalan dari berkas properti HOCON.
Dan tanpa membahas detail bagaimana mengimplementasikan aljabar, kita harus tahu
graf ketergantungan dari DynAgentsModule
.
Ada dua modul yang mengimplementasikan OAuth2JsonClient
, satu yang digunakan untuk
aljabar OAuth2 Refresh
(untuk Google) dan satunya yang menggunakan ulang BearerToken
tanpa kadaluarsa (untuk Drone).
Sampai disini, kita sudah melihat persyaratan untuk F
agar mempunyai Applicative[F]
,
Monad[F]
, dan MonadState[F, BearerToken]
. Semua persyaratan ini dapat dipenuhi
dengan menggunakan StateT[Task, BearerToken, ?]
sebagai konteks aplikasi kita.
Walaupun demikian, beberapa aljabar kita hanya mempunyai satu interpreter,
menggunakan Task
Namun harap diingat bahwa aljabar kita dapat menyediakan sebuah liftM
pada objek
pendampingnya, lihat pada bab 7.4 pada bagian Pustaka Transformator Monad, dan
memperkenankan kita untuk mengangkat LocalClock[Task]
pada konteks StateT[Task, BearerToken, ?]
dan pada akhirnya semuanya konsisten.
Sayangnya, cerita tidak berhenti disini. Beberapa hal menjadi semakin kompleks
saat kita beralih pada lapisan selanjutya. JsonClient
kita mempunyai sebuah
interpreter yang memiliki konteks yang berbeda
Harap perhatikan bahwa konstruktor BlazeJsonClient
mengembalikan sebuah
Task[JsonClient[F]]
dan bukan JsonClient[F]
. Hal ini disebabkan karena
pembuatan klien tersebut memiliki efek: kumpulan koneksi tak tetap dibuat dan
diatur secara internal oleh http4s.
Kita juga tidak boleh lupa bahwa kita harus menyediakan sebuah RefreshTokon
untuk
GoogleMachinesModule
. Kita dapat meminta pengguna untuk repot, namun karena kita
baik hati dan menyediakan aplikasi sekali pakai yang menggunakan aljabar Auth
dan Access
. Implementasi AuthModule
dan AccessModule
membawa ketergantungan
tambahan. Namun, tidak ada perubahan pada konteks aplikasi F[_]
.
Interpreter untuk UserInteraction
merupakan bagian paling kompleks dari
basis kode kita: bagian ini memulai peladen HTTP, mengirim pengguna untuk mengunjungi
sebuah laman web pada peramban mereka, menangkap panggilan balik pada peladen, dan
mengembalikan hasil sembari mematikan peladen web secara aman.
Kita tidak menggunakan StateT
untuk mengatur keadaan ini, namun kita menggunakan
primitif Promise
(dari ioeffect
). Kita harus selalu menggunakan Promise
atau IORef
, bukan StateT
bila kita menulis interpreter IO
. Tidak saja
StateT
memiliki dampak performa pada aplikasi utama, namun juga membocorkan
manajemen keadaan internal ke aplikasi utama, dan pada akhirnya harus bertanggung
jawab untuk menyediakan nilai awal. Kita juga tidak dapat menggunakan StateT
pada skenario ini karena kita membutuhkan semantik “menanti” yang hanya disediakan
oleh Promise
.
9.2 Main
Bagian paling buruk dari PF adalah memastikan bahwa semua monad selaras dan hal
semacam ini biasa terjadi pada titik mulai Main
.
Ikalan utama kita adalah
dan kabar baiknya, kode yang asli terlihat seperti
dimana F
menyimpan keadaan keseluruhan pada sebuah MonadState[F, WorldView]
.
Kita dapat menempatkannya pada sebuah metoda dengan nama .step
dan mengulang
selamanya dengan memanggil .step[F].forever[Unit]
.
Ada dua pendekatan yang dapat kita ambil, dan kita akan mempelajari keduanya.
Yang pertama, dan paling sederhana, adalah membangun sebuah susunan monad yang
sesuai dengan semua aljabar. Semua mendapatkan sebuah metoda .liftM
agar dapat
diangkat ke susunan yang lebih tinggi.
Kode yang ingin kita tulis untuk mode otentikasi sekali pakai adalah
dimana .readConfig
dan .putStrLn
merupakan panggilan pustaka. Kita dapat
menganggap mereka sebagai interpreter Task
untuk aljabar yang membaca konfigurasi
waktu-jalan dari aplikasi dan mencetak sebuah string ke layar.
Namun, kode ini tidak dapat dikompilasi karena dua alasan. Pertama, kita harus
mempertimbangkan bagaimana bentuk susunan monad kita. Konstruktor BlazeJsonClient
mengembalikan Task
namun metoda milik JsonClient
membutuhkan sebuah
MonadError[..., JsonClient.Error]
. Dan monad tersebut dapat disediakan oleh
EitherT
. Maka dari itu, kita dapat membangun susunan monad umum untuk semua
for comprehension sebagai
Sayangnya, hal ini juga berarti kita harus mengangkat semua yang mengembalikan
Task
dengan .liftM
. Hal semacam ini menambah plat cetak cukup banyak.
Sayangnya, metoda .liftM
tidak menerima tipe dengan bentuk H[_]
. .liftM
menerima tipe dengan bentuk H[_[_], _]
sehingga kita harus membuat sebuah
alias tipe untuk membantu kompilator:
sekarang kita dapat memanggil .liftM[HT]
saat kita menerima sebuah Task
Namun, kode diatas masih belum dapat dikompilasi karena clock
berupa LocalClock[Task]
dan AccessModule
membutuhkan sebuah LocalClock[H]
. Kita tinggal menambahkan
plat cetak .liftM
yang dibutuhkan pada objek pendamping dari LocalClock
dan
pada akhirnya dapat mengangkat semua aljabar
dan semuanya berhasil dikompilasi.
Pendekatan kedua adalah dengan membuat sebuah aplikasi yang lebih kompleks, namun dibutuhkan bila terjadi konflik pada susunan monad, seperti yang kita butuhkan pada ikalan utama kita. Bila kita melakukan analisis, kita akan menemukan bahwa monad berikutlah yang kita butuhkan:
-
MonadError[F, JsonClient.Error]
untuk penggunaanJsonClient
-
MonadState[F, BearerToken]
untuk penggunaanOAuth2JsonClient
-
MonadState[F, WorldView]
untuk ikalan utama kita
Sayangnya, persyaratan dua MonadState
menyebabkan konflik. Kita dapat membuat
sebuah tipe data yang menangkap semua keadaan program. Namun, hal tersebut merupakan
abstraksi yang penuh kebocoran. Maka dari itu, kita akan melapiskan komprehensi
for kita dan menyediakan keadaan saat dibutuhkan.
Sekarang kita harus berpikir mengenai tiga lapisan, yang kita sebut F
, G
, dan H
Dan sekarang saatnya berita buruk mengenai .liftM
. Metoda ini hanya berlaku
pada satu lapisan pada satu waktu. Bila kita mempunyai sebuah Task[A]
dan kita
ingin sebuah F[A]
, kita harus melangkahi semua lapisan dan menulis
ta.liftM[HT].liftM[GT].liftM[FT]
. Hal yang sama saat kita mengangkat aljabar,
kita harus memanggil liftM
berulang kali. Untuk mendapatkan Sleep[F]
, kita
harus menulis
dan untuk mendapatkan LocalClock[G]
kita harus melakukan dua kali pengangkatan
Dan aplikasi utama menjadi
dimana ikalan bagian luar menggunakan Task
, ikalan tengah menggunakan G
,
dan ikalan dalam menggunakan F
.
Panggilan ke .run(start)
dan .eval(bearer)
adalah dimana kita menyediakan
keadan awal untuk bagian StateT
aplikasi kita. .run
digunakan untuk menyingkap
galat EitherT
.
Kita dapat memanggil dua titik awal aplikasi ini dari SafeApp
kita
dan menjalankannya.
Hore!
9.3 Blaze
Kita mengimplementasikan klien dan peladen HTTP dengan pustaka pihak ketiga http4s
.
Interpreter untuk aljabar klien dan peladen disebut Blaze.
Kita butuh ketergantungan sebagai berikut
9.3.1 BlazeJsonClient
Sekarang kita butuh beberapa impor
Modul Client
dapat diringkas menjadi
dimana Request
dan Response
merupakan tipe data:
yang terdiri dari
Tipe EntityBody
merupakan alias untuk Stream
dari pustaka fs2
.
Tipe data Stream
dapat dianggap sebagai aliran data dengan efek yang ditarik secara
luntung. Tipe data ini diimplementasikan sebagai monad Free
dengan penangkapan
pengecualian dan interupsi. Stream
menerima dua parameter tipe: sebuah tipe
dengan efek dan sebuah tipe konten, dan memiliki representasi efisien internal
untuk mengelompokkan data. Sebagai contooh, walaupun kita menggunakan Stream[F, Byte]
sebenarnya monad ini membungkus Array[Byte]
yang tiba melalu jaringan.
Kita dapat mengkonversi header dan representasi URL kita menjadi versi yang dibutuhkan oleh http4s:
Metoda .get
dan .post
keduanya membutuhkan sebuah konversi dari tipe Response
http4s menjadi A
. Kita dapat memisahkannya menjadi sebuah fungsi,
.through(fs2.text.utf8Decode)
digunakan untuk mengkonversi Stream[Task, Byte]
menjadi Stream[Task, String]
dengan .compile.foldMonoid
menginterpretasinya
dengan Task
, dan pada akhirnya, menggabungkan semua bagian menggunakan Monoid[String]
.
Hasilnya adalah Task[String]
.
Lalu kita mengurai string tersebut sebagai JSON dan menggunakan JsDecoder[A]
untuk membuat keluaran yang dibutuhkan.
Berikut implementasi kita dari .get
.get
hanyalah berupa saluran: kita mengkonversi tipe masukan
menjadi http4s.Request
lalu memanggil .fetch
pada Client
dengan handler
kita. handler
mengembalikan sebuah Task[Error \/ A]
, namun kita membutuhkan
sebuah F[A]
. Maka dari itu, kita menggunakan MonadIO.liftIO
untuk membuat
F[Error \/ A]
dan melakukan pemetaan menggunakan .emap
untuk mendorong
galat ke F
.
Sayangnya, bila kita mencoba mengkompilasi kode ini, akan terjadi kegagalan. Galat akan terlihat seperti
Pada dasarnya, ada kucing yang hilang.
Alasan kegagalan ini adalah http4s menggunakan pustaka PF utama lain, bukan Scalaz.
Untungnya, scalaz-ioeffect
menyediakan lapisan kompatibilitas dan shims
yang menyediakan konversi implisit tanpa batas. Kita dapat mengkompilasi kode kita
dengan ketergantungan sebagai berikut:
dan mengimpor
Implementasi .post
kurang lebih sama. Namun, kita juga harus menyediakan
instans dari
Untungnya, kelas tipe EntityEncoder
menyediakan metoda bantuan agar dapat
memperkenankan kita untuk menderivasi dari penyandi String
yang sudah ada
Satu-satunya pembeda antara .get
dan .post
adalah cara kita membangun http4s.Request
dan bagian utama adalah pembangun, yang hanya berupa pemanggilan Http1Client
dengan objek konfigurasi
9.3.2 BlazeUserInteraction
Kita harus menyalakan sebuah peladen HTTP, yang sebenarnya jauh lebih mudah bila dibandingkan yang terdengar. Pertama, kita mengimpor
Kita harus membuat sebuah dsl
untuk tipe efek kita, yang nantinya akan kita
impor
Sekarang, kita dapat menggunakan dsl http4s untuk membuat titik akhir HTTP. Kita tidak akan mendeskripsikan apa yang kita lakukan, kita hanya perlu mengimplementasikannya. Titik akhir ini mirip dengan DSL HTTP lain
Tipe kembalian untuk tiap pencocokan pola adalah sebuah Task[Response[Task]]
.
Pada implementasi kita, kita menginginkan untuk menerima code
dan menempatkannya
pada promise ptoken
:
namun, definisi dari rute layanan kita masih belum cukup. Kita harus menjalankan
sebuah peladen, yang dapat kita lakukan dengan BlazeBuilder
Dengan mengikat layanan ke port 0
, kita meminta kepada sistem operasi untuk
menetapkan port manapun. Kita dapat menemukan port mana yang sebenarnya berjalan
dengan melakukan kueri pada bidang server.address
.
Implementasi kita atas metoda .start
dan .stop
pun tidak banyak basa-basi
1.second
sleep penting untuk menghindari matinya peladen sebelum respons
dikirimkan balik ke peramban. IO tidak pernah main-main bila kita berbicara
mengenai performa konkurensi.
Dan pada akhirnya, untuk membuat sebuah BlazeUserInteraction
, kita hanya perlu
dua promise yang belum dimulai
Kita bisa saja menggunakan IO[Void, ?]
, namun karena bagian-bagian aplikasi
kita lainnya menggunakan Task
(mis, IO[Throwable, ?]
), kita dapat memperluas
cakupan galat dengan menggunakan .widenError
agar kita dapat menghindari
pengenalan plat cetak baru sehingga fokus kita kembali terpecah.
9.4 Terima Kasih
Demikian! Kami ucapkan selamat kepada pembaca yang selesai membaca sampai akhir.
Bila pembaca budiman mempelajari sesuatu dari buku ini, mohon untuk memberi tahu handai-taulan dan kawan-kawan mengenai buku ini. Buku ini tidak memiliki Bagian Pemasaran, sehingga promosi dari-mulut-ke-mulut sajalah pembaca lain dapat tahu.
Pembaca budiman juga dapat ikut serta atas pengembangan Scalaz dengan bergabung pada ruang obrolan gitter. Dari sini, pembaca dapat meminta saran, membantu pengguna baru (karena pembaca budiman sudah ahli), dan berkontribusi untuk rilis selanjutnya.
Contekan Kelas Tipe
Kelas Tipe | Metoda | Asal | Diberikan | Tujuan |
---|---|---|---|---|
InvariantFunctor |
xmap |
F[A] |
A => B, B => A |
F[B] |
Contravariant |
contramap |
F[A] |
B => A |
F[B] |
Functor |
map |
F[A] |
A => B |
F[B] |
Apply |
ap / <*>
|
F[A] |
F[A => B] |
F[B] |
apply2 |
F[A], F[B] |
(A, B) => C |
F[C] |
|
Alt |
altly2 |
F[A], F[B] |
(A \/ B) => C |
F[C] |
Divide |
divide2 |
F[A], F[B] |
C => (A, B) |
F[C] |
Decidable |
choose2 |
F[A], F[B] |
C => (A \/ B) |
F[C] |
Bind |
bind / >>=
|
F[A] |
A => F[B] |
F[B] |
join |
F[F[A]] |
F[A] |
||
Cobind |
cobind |
F[A] |
F[A] => B |
F[B] |
cojoin |
F[A] |
F[F[A]] |
||
Applicative |
point |
A |
F[A] |
|
Divisible |
conquer |
F[A] |
||
Comonad |
copoint |
F[A] |
A |
|
Semigroup |
append |
A, A |
A |
|
Plus |
plus / <+>
|
F[A], F[A] |
F[A] |
|
MonadPlus |
withFilter |
F[A] |
A => Boolean |
F[A] |
Align |
align |
F[A], F[B] |
F[A \&/ B] |
|
merge |
F[A], F[A] |
F[A] |
||
Zip |
zip |
F[A], F[B] |
F[(A, B)] |
|
Unzip |
unzip |
F[(A, B)] |
(F[A], F[B]) |
|
Cozip |
cozip |
F[A \/ B] |
F[A] \/ F[B] |
|
Foldable |
foldMap |
F[A] |
A => B |
B |
foldMapM |
F[A] |
A => G[B] |
G[B] |
|
Traverse |
traverse |
F[A] |
A => G[B] |
G[F[B]] |
sequence |
F[G[A]] |
G[F[A]] |
||
Equal |
equal / ===
|
A, A |
Boolean |
|
Show |
shows |
A |
String |
|
Bifunctor |
bimap |
F[A, B] |
A => C, B => D |
F[C, D] |
leftMap |
F[A, B] |
A => C |
F[C, B] |
|
rightMap |
F[A, B] |
B => C |
F[A, C] |
|
Bifoldable |
bifoldMap |
F[A, B] |
A => C, B => C |
C |
(with MonadPlus ) |
separate |
F[G[A, B]] |
(F[A], F[B]) |
|
Bitraverse |
bitraverse |
F[A, B] |
A => G[C], B => G[D] |
G[F[C, D]] |
bisequence |
F[G[A], G[B]] |
G[F[A, B]] |
Haskell
Scalaz documentation often cites libraries or papers written in the Haskell programming language. In this short chapter, we will learn enough Haskell to be able to understand the source material, and to attend Haskell talks at functional programming conferences.
Dokumentasi Scalaz sering kali mengutip pustaka atau makalah yang ditulis dengan bahasa pemrograman Haskell. Pada bab pendek ini, kita akan mempelajari Haskell agar dapat memahami materi sumber, dan dapat mengunjungi pembahasan haskell pada konferensi pemrograman fungsional.
Data
Haskell memiliki sintaks yang jelas untuk Tipe Data Aljabaris. Berikut adalah struktur senarai berantai:
List
merupakan konstruktor tipe, a
merupakan parameter tipe, |
memisahkan
konstruktor data, yang terdiri dari: Nil
yang merupakan senarai kosong dan
Cons
yang menerima dua parameter yang dipisahkan ruang putih: tanpa koma dan
tanpa pengurung parameter. Selain itu, Haskell juga tidak memiliki anak-tipe.
Bila diterjemahkan ke Scala, kurang lebih sebagai berikut:
mis., konstruktor tipe bisa kurang lebih seperti sealed abstract class
, dan
tiap konstruktor data sebagai .apply
/ .unapply
. Harap diperhatikan bahwa
Scala tidak melakukan pencocokan pola pada penyandian semacam ini. Dengan
demikian, Scalaz juga tidak menggunakannya.
Bila kita ingin mendefinisikan List
yang lebih rapi, kita dapat menggunakan
simbol infiks :.
sebagai ganti Cons
dimana kita menentukan ketetapan (fixity) dimana infix
untuk menentukan
tidak adanya hubungan asosiatif, infixl
untuk hubungan asosiatif kiri, dan
infixr
untuk hubungan asosiatif kanan. Angka dari 0 (longgar) sampai 9 (ketat)
menentukan presedensi. Sekarang kita dapat membuat senarai integer dengan menulis
Haskell sudah mengikut sertakan dukungan senarai berantai, yang sangat fundamental
pada pemrograman fungsional, sampai pada tingkat bahasa dengan memberikan sintaks
kurung siku sehingga dilambangkan dengan [a]
dan pembantu konstruktor nilai argumen jamak: [1, 2, 3]
, bukan 1 : 2 : 3 : []
.
Dan utamanya, Tipe Data Aljabaris kita harus menampung nilai primitif. Tipe data primitif yang paling jamak digunakan adalah:
-
Char
karakter unikode -
Text
untuk blok teks unikode -
Int
integer tertanda dengan presisi tetap yang bergantung pada mesin -
Word
Int
tanpa tanda, danWord8
/Word16
/Word32
/Word64
dengan ukuran tetap -
Float
/Double
bilangan presisi tunggal dan ganda berstandar IEEE -
Integer
/Natural
integer tertanda presisi arbiter / non-negatif -
(,)
tuple, dari 0 (disebut juga unit) sampai 62 bidang -
IO
inspirasi dariIO
Scalaz, diimplementasikan pada waktu-jalan.
dengan sebutan kehormatan untuk
Seperti Scala, Haskell memiliki alias tepe: sebuah alias atau bentuk terjabarkannya
dapat digunakan secara bergantian. Dikarenakan alasan peninggalan, String
didefinisikan sebagai senarai berantai dari Char
yang sangat tidak efisien. Kami sangat menyarankan untuk menggunakan Text
sebagai gantinya.
Dan pada akhirnya, kita dapat mendefinisikan nama bidang pada TDA dengan menggunakan sintaks rekor, yang juga berarti, kita dapat menampung konstruktor data didalam kurung kurawal dan menggunakan anotasi tipe dua titik dua untuk mengindikasikan tipe dari bidang tersebut
Harap perhatikan bahwa konstruktor data Human
dan tipe Resource
tidak harus
memiliki nama yang sama. Sintaks rekor membuat ekuivalen dari metoda pengakses
bidang dan penyalinan.
Alternatif yang lebih efisien untuk pendefinisian data
dengan satu bidang
saja adalah dengan menggunakan newtype
yang tidak meminta beban tambahan
saat waktu-jalan:
yang ekuivalen dengan extends AnyVal
namun tanpa kekurangannya.
Fungsi
Walaupun tidak wajib, menuliskan penanda tipe dari sebuah fungsi secara eksplisit
merupakan kebiasaan yang bagus: nama fungsi diikuti tipenya. Sebagai contoh
foldl
yang dispesialisasikan untuk senarai berantai
Semua fungsi di-curry-kan pada Haskell, tiap parameter dipisahkan oleh sebuah
->
dan tipe paling akhir merupakan tipe kembalian. Penanda tipe diatas ekuvalen
dengan penanda tipe pada Scala:
Bebarapa pengamatan:
- tidak ada kata kunci
- tidak diperlukannya tipe yang digunakan
- tidak diperlukannya nama parameter
yang membuat kode ringkas
Fungsi infiks didefinisikan dalam tanda kurung dan membutuhkan definisi ketetapan:
Regular functions can be called in infix position by surrounding their name with backticks, and an infix function can be called like a regular function if we keep it surrounded by brackets. The following are equivalent:
Fungsi biasa dapat dipanggil pada posisi infiks dengan mengurung nama fungsi tersebut dengan tanda petik. Begitu juga dengan fungsi infiks yang bisa dipanggil sebagaimana fungsi pada umumnya dengan mengurungnya dengan tanda kurung. Berikut adalah ekuivalen:
Sebuah fungsi infiks dapat di-curry-kan pada bagian kiri maupun kanan yang sering kali memberikan semantik berbeda:
Fungsi biasanya ditulis dengan parameter yang paling umum sebagai parameter awal, agar dapat digunakan berulang kali dalam bentuk ter-curry.
Definisi dari sebuah fungsi bisa digunakan untuk pencocokan pola, dengan satu
baris untuk tiap kasus. Disinilah kita menamai parameter dengan menggunakan
konstruktor data untuk mengekstrak parameter, seperti klausa case
milik Scala:
Garis bawah merupakan placeholder untuk parameter yang diabaikan dan nama fungsi bisa diletakkan pada posisi infiks:
Kita dapat mendefinisikan fungsi lambda anonim dengan sebuah garis miring terbalik, yang bila kita maksa akan terlihat seperti huruf Yunani 位. Tiap baris berikut adalah ekuivalen:
Fungsi Haskell yang tercocokkan berdasarkan pola hanya merupakan pemanis sintaks dari fungsi lambda berlapis. Anggap fungsi sederhana berikut yang membuat sebuah tupel ketika diberikan tiga buah masukan:
Implementasi
dijabarkan menjadi
Pada isi sebuah fungsi, kita dapat membuat sebuah penetapan fungsi lokal dengan
menggunkan klausa let
maupun where
. Potongan kode berikut merupakan definisi
yang ekuivalen dari map untuk senarai berantai (tanpa petik merupakan nama
identifier yang valid):
if
/ then
/ else
merupakan kata kunci untuk statemen kondisional:
Namun penggunaan pembatas case dianggap sebagai gaya superior
Pencocokan pola pada tiap istilah dilakukan dengan menggunakan case ... of
Pembatas juga dapat digunakan pada pencocokan. Misalkan, kita ingin mengkhususkan nol:
Dan pada akhirnya, dua fungsi yang patut diperhatikan adalah ($)
dan (.)
Kedua fungsi ini merupakan gaya penggunaan alternatif dari penggunaan tanda kurung berlapis.
Potongan berikut setara:
sebagaimana
Ada kecenderungan untuk menggunakan komposisi fungsi dengan .
bila dibandingkan
dengan penggunaan $
jamak
Kelas Tipe
Untuk mendefiniskan kelas tipe, kita menggunakan kata kunci class
yang diteruskan
dengan nama kelas, parameter tipenya, dan anggota yang dibutuhkan pada klausa
where
. Bila ada ketergantungan antar kelas tipe, misalkan Applicative
membutuhkan Functor
, gunakan notasi =>
Kita menyediakan implementasi dari kelas tipe dengan kata kunci instance
. Bila
kita ingin mengulang penanda tipe pada fungsi instans, demi kejelasan, kita
harus menggunakan ekstensi InstanceSigs
.
Bila kita ingin menggunakan kelas tipe pada fungsi, kita menggunakan =>
pada
penanda tipenya. Sebagai contoh, kita dapat mendefinisikan fungsi yang mirip
dengan Apply.apply2
milik Scalaz sebagai berikut
Karena kita telah memperkenalkan Monad
, saatnya memperkenalkan notasi do
yang merupakan inspirasi untuk komprehensi for Scala:
yang dijabarkan menjadi
dimana >>=
adalah =<<
dengan parameter yang dibalik
dan return
sebagai sinonim untuk pure
.
Tidak seperti Scala, kita tidak perlu mengikat nilai unit, atau menyediakan yield
bilakita mengembalikan ()
. Sebagai contoh
menjadi
Nilai non-monadik dapat ditetapkan dengan kata kunci let
:
Haskell juga mempunyai derivasi kelas tipe yang menggunakan kata kunci deriving
yang juga merupakan inspirasi untuk @scalaz.deriving
. Mendefinisikan aturan
derivasi merupakan topik lanjutan. Namun, untuk menderivasi kelas tipe untuk
sebuah tipe data aljabaris sangat mudah:
Modul
Sumber kode Haskell diatur menjadi modul hierarkis dengan batasan bahwa semua
konten dari sebuah module
harus ada pada sebuah berkas. Pada bagian atas sebuah
berkas, nama module
dideklarasikan
Direktori digunakan pada diska untuk mengelompokkan kode, jadi berkas ini
harus berada pada Silly/Tree.hs
.
Secara default, semua simbol pada berkas akan diekspor. Namun, kita dapat
menentukan mana yang akan diekspor. Sebagai contoh, tipe Tree
dan konstruktor
data, fungsi fringe
, dan melewatkan sapling
:
Yang menarik adalah, kita dapat mengekspor simbol yang juga diimpor ke modul tersebut. Hal ini memperkenankan penulis pustaka untuk mengemas seluruh APA mereka menjadi satu modul, terlepas bagaimana APA tersebut diimplementasikan.
Pada berkas yang berbeda, kita dapat mengimpor semua anggota dari Silly.Tree
yang kurang lebih setara dengan sintaks import silly.tree._
milik Scala. Bila
kita ingin membatasi simbol yang kita impor, kita dapat menyediakan daftar eksplisit
didalam tanda kurung setelah impor tersebut
Bila kita mendapati nama yang bentrok pada sebuah simbol, kita dapat menggunakan
impor qualified
dengan daftar opsional dari simbol yang diimpor
dan sekarang bila kita memanggil fungsi fringe
, kita dapat menuliskan Silly.Tree.fringe
sebagai ganti dari fringe
. Kita juga dapat mengubah nama modul saat mengimpornya
dengan
Fungsi fringe
sekarang dapat dipanggil dengan T.fringe
Bisa juga kita memilih untuk tidak mengimpor simbol tertentu
Secara default, module Prelude
selalu diimpor secara implisit. Namun, bila kita
secara eksplisit mengimpor module Prelude
, maka versi kita yang akan dipakai.
Kita bisa menggunakan teknik ini bila kita ingin menyembunyikan fungsi peninggalan
atau menggunakan mukadimah (Prelude
) khusus dan menon-aktifkan mukadimah default
dengan ekstensi bahasa
Evaluasi
Haskell mengkompilasi kode menjadi kode yang berjalan tanpa mesin virtual. Namun, ada sebuah pengoleksi sampah. Aspek fundamental dari waktu-jalan Haskell adalah semua parameter dievaluasi secara landung secara default. Haskell hanya menjanjikan sebuah nilai hanya disediakan bila diperlukan dalam bentuk sub-rutin thunk. Thunk hanya dikurangi bila diperlukan.
Keuntungan utama dari evaluasi landung adalah stack overflow akan lebih sulit terpicu. Kerugiannya adalah akan ada beban tambahan bila dibandingkan dengan evaluasi tegas adalah. Sebagai penyiasatan seperti ini, kita dapat memilih evaluasi tugas per parameter.
Haskell juga agak sedikit berbeda mengenai arti dari evaluasi tegas: sebuah istilah dikatakan weak head normal-form (WHNF) bila blok kode terluar tidak dapat direduksi lebih lanjut, dan normal form bila sebuah istilah dapat dievaluasi seutuhnya. Strategi evaluasi default Scala kurang lebih sesuai dengan normal form.
Sebagai contoh, istilah berikut merupakan normal form:
sedangkan istilah berikut bukan dalam bentuk normal (dapat direduksi lebih lanjut):
Istilah berikut berbentuk WHNF karena blok kode terluar tidak dapat direduksi lebih lanjut (walaupun bagian dalam dapat direduksi):
dan potongan berikut tidak dalam bentuk WHNF
Strategi evaluasi default adalah dengan dengan tidak melakukan reduksi ketika
mengumpankan sebuah istilah sebagai parameter. Dukungan bahasa memperkenankan
kita untuk meminta WHNF untuk semua istilah dengan ($!)
Kita juga dapat menggunakan tanda seru !
pada parameter data
Ekstensi bahasa StrictData
menjadikan semua parameter data pada modul menjadi
tegas.
Ekstensi lain, BangPatterns
, memperkenankan !
digunakan pada argumen fungsi.
Ekstensi bahasa Strict
membuat semua parameter fungsi dan data pada modul menjadi
tegas secara default.
Bila kita maksa, kita dapat menggunakan ($!!)
dan kelastipe NFData
untuk
evaluasi bentuk normal:
yang menjadi subjek ketersedian dari sebuah instans NFData
.
Beban dari ketegasan semacam ini adalah Haskell berperilaku sebagaimana bahasa tegas lainnya dan mungkin saja melakukan tugas yang tak perlu. Memilih ketegasan harus dilakukan secara hati hati dan setelah dibuktikan adanya peningkatan performa. Bila masih ragu, mundur saja.
Langkah Selanjutnya
Haskell merupakan bahasa yang lebih cepat, aman, dan sederhana bila dibandingkan
dengan Scala. Selain itu, Haskell sudah terbukti di industri. Pertimbangkan untuk
mengambil kursus pemrograman fungsional data61
dan bertanya pada ruang obrolan #qfpl
di freenode.net
.
Beberapa materi pelajaran tambahan adalah:
- Haskell Book merupakan materi pengenalan yang sangat komprehensif, atau Programming in Haskell untuk pengantar yang lebih singkat. a faster ride.
- Parallel and Concurrent Programming in Haskell dan What I Wish I Knew When Learning Haskell untuk petuah bijak.
- Glasgow Haskell Compiler User Guide dan HaskellWiki untuk dokumentasi mendetail.
- Eta, Haskell untuk JVM.
Bila pembaca budiman menggunakan Haskell dan memahami nilai yang ditawarkan pada bisnis pembaca, maka silakan sampaikan kepada manajer. Dengan demikian, beberapa manajer yang membawahi proyek Haskell akan menarik bakat-bakat pemrograman fungisonal dari banyak tim yang tidak. Dan pada akhirnya, bapak senang, ibu senang, di sini senang, di mana senang, semua senang!
Lisensi Pihak Ketiga
Beberapa sumber kode pada buku ini disalin dari proyek perangkat lunak bebas. Lisensi proyek tersebut meminta teks berikut didistribusikan bersama sumber yang diditunjukkan pada buku ini.