Pemrograman fungsional dalam Scala mungkin sulit untuk dikuasai karena beberapa fitur sintaksis dan semantik bahasa. Secara khusus, beberapa alat bahasa dan cara untuk mengimplementasikan apa yang Anda rencanakan dengan bantuan perpustakaan utama tampak jelas ketika Anda terbiasa dengan mereka - tetapi pada awal belajar, terutama pada Anda sendiri, tidak begitu mudah untuk mengenali mereka.
Untuk alasan ini, saya memutuskan akan berguna untuk berbagi beberapa tips pemrograman fungsional di Scala. Contoh dan nama sesuai dengan kucing, tetapi sintaksis dalam scalaz harus sama karena dasar teoretis umum.

9) Konstruktor metode ekstensi
Mari kita mulai dengan, mungkin, alat paling dasar - metode ekstensi dari jenis apa pun yang mengubah instance menjadi Option, Either, dll., Khususnya:
.some
dan none
metode konstruktor yang sesuai untuk Option
;.asRight
, .asLeft
for Either
;.valid
, .invalid
, .validNel
, .invalidNel
untuk Validated
Dua keuntungan utama penggunaannya:
- Ini lebih ringkas dan mudah dimengerti (karena urutan pemanggilan metode disimpan).
- Tidak seperti opsi konstruktor, tipe kembalinya metode ini diperluas ke supertype, yaitu:
import cats.implicits._ Some("a")
Meskipun inferensi tipe telah meningkat selama bertahun-tahun, dan jumlah situasi yang memungkinkan perilaku ini membantu programmer untuk tetap tenang telah menurun, kesalahan kompilasi karena pengetikan yang terlalu khusus masih dimungkinkan di Scala hari ini. Cukup sering, keinginan untuk membenturkan kepala ke meja muncul ketika bekerja dengan salah
Either
(lihat
Scala dengan Kucing bab 4.4.2).
Satu hal lagi pada topik:
.asRight
dan
.asLeft
masih memiliki satu parameter tipe lagi. Misalnya,
"1".asRight[Int]
adalah
Either[Int, String]
. Jika parameter ini tidak disediakan, kompiler akan mencoba untuk mengeluarkannya dan tidak mendapatkan
Nothing
. Namun demikian, ini lebih nyaman daripada memberikan kedua parameter setiap kali atau tidak menyediakan, seperti dalam kasus konstruktor.
8) Lima puluh warna *>
Operator *> yang didefinisikan dalam setiap metode
Apply
(yaitu, dalam
Applicative
,
Monad
, dll.) Secara sederhana berarti "memproses perhitungan awal dan mengganti hasilnya dengan apa yang ditentukan dalam argumen kedua". Dalam bahasa kode (dalam kasus
Monad
):
fa.flatMap(_ => fb)
Mengapa menggunakan operator simbolis yang tidak jelas untuk operasi yang tidak memiliki efek nyata? Mulai menggunakan ApplicativeError dan / atau MonadError, Anda akan menemukan bahwa operasi mempertahankan efek kesalahan untuk seluruh alur kerja. Ambil
Either
sebagai contoh:
import cats.implicits._ val success1 = "a".asRight[Int] val success2 = "b".asRight[Int] val failure = 400.asLeft[String] success1 *> success2
Seperti yang Anda lihat, bahkan jika terjadi kesalahan, perhitungannya tetap pendek. *> akan membantu Anda dengan pekerjaan dengan perhitungan yang ditangguhkan dalam
Monix
,
IO
dan sejenisnya.
Ada operasi simetris, <*. Jadi, dalam kasus contoh sebelumnya:
success1 <* success2
Akhirnya, jika penggunaan simbol adalah asing bagi Anda, tidak perlu menggunakan itu. *> Hanya alias untuk
productR
, dan * <adalah alias untuk
productL
.
Catatan
Dalam percakapan pribadi, Adam Warski (terima kasih, Adam!) Dengan tepat mengatakan bahwa selain *> (
productR
) juga ada >> dari
FlatMapSyntax
. >> didefinisikan dengan cara yang sama dengan
fa.flatMap(_ => fb)
, tetapi dengan dua nuansa:
- itu didefinisikan secara independen dari
productR
, dan oleh karena itu, jika karena alasan tertentu kontrak metode ini berubah (secara teoritis, itu dapat diubah tanpa melanggar hukum monadik, tetapi saya tidak yakin tentang MonadError
), Anda tidak akan menderita; - yang lebih penting, >> memiliki operan kedua yang dipanggil oleh panggilan-dengan-nama, yaitu
fb: => F[B]
. Perbedaan dalam semantik menjadi mendasar jika Anda melakukan perhitungan yang dapat menyebabkan ledakan tumpukan.
Berdasarkan ini, saya mulai menggunakan *> lebih sering. Dengan satu atau lain cara, jangan lupa tentang faktor-faktor yang tercantum di atas.
7) Angkat layar!
Banyak yang meluangkan waktu untuk memasukkan konsep
lift
ke dalam kepala mereka. Tetapi ketika Anda berhasil, Anda akan menemukan bahwa dia ada di mana-mana.
Seperti banyak istilah menjulang di udara pemrograman fungsional,
lift
datang dari
teori kategori . Saya akan mencoba menjelaskan: melakukan operasi, mengubah tanda tangan dari tipenya sehingga menjadi terkait langsung dengan tipe abstrak F.
Dalam Kucing, contoh paling sederhana adalah
Functor :
def lift[A, B](f: A => B): F[A] => F[B] = map(_)(f)
Ini berarti: ubah fungsi ini sehingga bekerja pada tipe functor F.
Fungsi lift seringkali identik dengan konstruktor bersarang untuk jenis tertentu. Jadi,
EitherT.liftF
pada dasarnya
EitherT.right.
Contoh dari Scaladoc :
import cats.data.EitherT import cats.implicits._ EitherT.liftF("a".some)
Cherry on the cake:
lift
hadir di mana-mana di perpustakaan standar Scala. Contoh paling populer (dan mungkin yang paling berguna dalam pekerjaan sehari-hari) adalah
PartialFunction
:
val intMatcher: PartialFunction[Int, String] = { case 1 => "jak się masz!" } val liftedIntMatcher: Int => Option[String] = intMatcher.lift liftedIntMatcher(1)
Sekarang kita bisa beralih ke masalah yang lebih mendesak.
6) petaN
mapN
adalah fungsi pembantu yang berguna untuk bekerja dengan tuple. Sekali lagi, ini bukan hal baru, tetapi pengganti operator tua yang baik
|@|
Dia adalah Jeritan.
Inilah yang terlihat seperti mapN dalam hal dua elemen:
Pada dasarnya, ini memungkinkan kita untuk memetakan nilai-nilai di dalam tuple dari F apa pun yang merupakan kelompok (produk) dan functor (peta). Jadi:
import cats.implicits._ ("a".some, "b".some).mapN(_ ++ _)
Ngomong-ngomong, jangan lupa bahwa dengan kucing Anda mendapatkan peta dan peta
leftmap
untuk tupel:
("a".some, List("b","c").mapN(_ ++ _))
Fungsi
.mapN
berguna lainnya adalah instantiating kelas kasus:
case class Mead(name: String, honeyRatio: Double, agingYears: Double) ("półtorak".some, 0.5.some, 3d.some).mapN(Mead) //Some(Mead(półtorak,0.5,3.0))
Tentu saja, Anda lebih suka menggunakan operator loop untuk ini, tetapi mapN menghindari transformator monadik dalam kasus sederhana.
import cats.effect.IO import cats.implicits._
Metode memiliki hasil yang serupa, tetapi yang terakhir dikeluarkan dengan trafo monadik.
5) Bersarang
Nested
pada dasarnya adalah double umum dari trafo monad. Seperti namanya, ini memungkinkan Anda untuk melakukan operasi lampiran dalam kondisi tertentu. Ini adalah contoh untuk
.map(_.map( :
import cats.implicits._ import cats.data.Nested val someValue: Option[Either[Int, String]] = "a".asRight.some Nested(someValue).map(_ * 3).value
Selain
Functor
,
Nested
menggeneralisasikan
Applicative
,
ApplicativeError
dan
Traverse
. Informasi dan contoh tambahan ada di
sini .
4) .recover / .recoverWith / .handleError / .handleErrorWith / .valueOr
Pemrograman fungsional dalam Scala banyak berkaitan dengan penanganan efek kesalahan.
ApplicativeError
dan
MonadError
memiliki beberapa metode yang berguna, dan mungkin berguna bagi Anda untuk mengetahui perbedaan halus antara empat yang utama. Jadi, dengan
ApplicativeError F[A]:
handleError
mengubah semua kesalahan pada titik panggilan ke A sesuai dengan fungsi yang ditentukan.recover
tindakan dengan cara yang serupa, tetapi menerima fungsi parsial, dan karenanya dapat mengubah kesalahan yang Anda pilih menjadi A.handleErrorWith
mirip dengan handleError
, tetapi hasilnya akan terlihat seperti F[A]
, yang artinya membantu Anda mengonversi kesalahan.recoverWith
bertindak seperti memulihkan, tetapi juga membutuhkan F[A]
sebagai hasilnya.
Seperti yang Anda lihat, Anda dapat membatasi
handleErrorWith
untuk
handleErrorWith
dan
recoverWith
, yang mencakup semua fungsi yang mungkin. Namun, masing-masing metode memiliki kelebihan dan nyaman dengan caranya sendiri.
Secara umum, saya menyarankan Anda untuk membiasakan diri dengan API
ApplicativeError , yang merupakan salah satu yang terkaya di Kucing dan diwarisi di MonadError - yang berarti didukung di
cats.effect.IO
,
monix.Task
, dll.
Ada metode lain untuk
Either/EitherT
,
Validated
dan
Ior
-
.valueOr
. Pada dasarnya, ini berfungsi seperti
.getOrElse
untuk
Option
, tetapi generik untuk kelas yang berisi sesuatu "ke kiri".
import cats.implicits._ val failure = 400.asLeft[String] failure.valueOr(code => s"Got error code $code")
3) kucing gang
gang-kucing adalah solusi yang mudah untuk dua kasus:
- contoh kelas ubin yang tidak mengikuti hukumnya 100%;
- Typklassy tambahan yang tidak biasa, yang dapat digunakan dengan benar.
Secara historis, contoh monad untuk
Try
paling populer di proyek ini, karena
Try
, seperti yang Anda tahu, tidak memenuhi semua hukum monadik dalam hal kesalahan fatal. Sekarang dia benar-benar diperkenalkan dengan Kucing.
Meskipun demikian, saya sarankan Anda membiasakan diri dengan
modul ini , mungkin terasa bermanfaat bagi Anda.
2) Perlakukan impor secara bertanggung jawab
Anda harus tahu - dari dokumentasi, buku, atau dari tempat lain - bahwa kucing menggunakan hierarki impor tertentu:
cats.x
untuk
cats.x
dasar (kernel);
cats.data
untuk tipe data seperti Validated, monad transformers, dll.
cats.syntax.x._ untuk mendukung metode ekstensi sehingga Anda dapat memanggil sth.asRight, sth.pure, dll.;
cats.instances.x.
_ untuk secara langsung mengimpor implementasi dari berbagai typclass ke dalam lingkup implisit untuk tipe beton individu sehingga ketika memanggil, misalnya, sth.pure, kesalahan "implisit tidak ditemukan" tidak terjadi.
Tentu saja, Anda memperhatikan impor
cats.implicits._
, yang mengimpor semua sintaks dan semua instance dari kelas tipe dalam cakupan implisit.
Pada prinsipnya, saat mengembangkan dengan Kucing, Anda harus mulai dengan urutan impor tertentu dari FAQ, yaitu:
import cats._ import cats.data._ import cats.implicits._
Jika Anda mengenal perpustakaan dengan lebih baik, Anda bisa memadukannya dengan selera Anda. Ikuti aturan sederhana:
cats.syntax.x
menyediakan sintaksis ekstensi yang terkait dengan x;cats.instances.x
menyediakan kelas instan.
Misalnya, jika Anda memerlukan
.asRight
, yang merupakan metode ekstensi untuk
Either
, lakukan hal berikut:
import cats.syntax.either._ "a".asRight[Int]
Di sisi lain, untuk mendapatkan
Option.pure
Anda harus mengimpor
cats.syntax.monad
DAN cats.instances.option
:
import cats.syntax.applicative._ import cats.instances.option._ "a".pure[Option]
Dengan mengoptimalkan impor secara manual, Anda akan membatasi cakupan implisit dalam file Scala Anda dan dengan demikian mengurangi waktu kompilasi.
Namun, tolong: jangan lakukan ini jika kondisi berikut tidak terpenuhi:
- Anda sudah menguasai Kucing dengan baik
- tim Anda memiliki perpustakaan di tingkat yang sama
Mengapa Karena:
Ini karena baik
cats.implicits
maupun
cats.instances.option
adalah ekstensi dari
cats.instances.OptionInstances
. Bahkan, kami mengimpor cakupan implisitnya dua kali, daripada kami mengacaukan kompiler.
Selain itu, tidak ada keajaiban dalam hierarki implisit - ini adalah urutan ekstensi tipe yang jelas. Anda hanya perlu merujuk definisi
cats.implicits
dan memeriksa hierarki jenis.
Selama 10-20 menit Anda dapat mempelajarinya cukup untuk menghindari masalah seperti ini - percayalah, investasi ini pasti akan terbayar.
1) Jangan lupa tentang pembaruan kucing!
Anda mungkin berpikir perpustakaan FP Anda tidak lekang oleh waktu, tetapi sebenarnya
cats
dan
scalaz
memperbarui secara aktif. Ambil contoh kucing. Berikut ini hanyalah perubahan terbaru:
Karena itu, ketika bekerja dengan proyek, jangan lupa untuk memeriksa versi perpustakaan, baca catatan untuk versi baru dan perbarui dalam waktu.