9 tips untuk menggunakan perpustakaan Kucing di Scala

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:

  1. Ini lebih ringkas dan mudah dimengerti (karena urutan pemanggilan metode disimpan).
  2. Tidak seperti opsi konstruktor, tipe kembalinya metode ini diperluas ke supertype, yaitu:

 import cats.implicits._ Some("a") //Some[String] "a".some //Option[String] 

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 //Right(b) success2 *> success1 //Right(a) success1 *> failure //Left(400) failure *> success1 //Left(400) 

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 //Right(a) 

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) //EitherT(Some(Right(a))) EitherT.liftF(none[String]) //EitherT(None) 

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) //Some(jak się masz!) liftedIntMatcher(0) //None intMatcher(1) //jak się masz! intMatcher(0) //Exception in thread "main" scala.MatchError: 0 

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:

 // where t2: Tuple2[F[A0], F[A1]] def mapN[Z](f: (A0, A1) => Z)(implicit functor: Functor[F], semigroupal: Semigroupal[F]): F[Z] = Semigroupal.map2(t2._1, t2._2)(f) 

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(_ ++ _) //Some(ab) (List(1, 2), List(3, 4), List(0, 2).mapN(_ * _ * _)) //List(0, 6, 0, 8, 0, 12, 0, 16) 

Ngomong-ngomong, jangan lupa bahwa dengan kucing Anda mendapatkan peta dan peta leftmap untuk tupel:

 ("a".some, List("b","c").mapN(_ ++ _)) //won't compile, because outer type is not the same ("a".some, List("b", "c")).leftMap(_.toList).mapN(_ ++ _) //List(ab, ac) 

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._ //interchangable with eg Monix's Task type Query[T] = IO[Option[T]] def defineMead(qName: Query[String], qHoneyRatio: Query[Double], qAgingYears: Query[Double]): Query[Mead] = (for { name <- OptionT(qName) honeyRatio <- OptionT(qHoneyRatio) agingYears <- OptionT(qAgingYears) } yield Mead(name, honeyRatio, agingYears)).value def defineMead2(qName: Query[String], qHoneyRatio: Query[Double], qAgingYears: Query[Double]): Query[Mead] = for { name <- qName honeyRatio <- qHoneyRatio agingYears <- qAgingYears } yield (name, honeyRatio, agingYears).mapN(Mead) 

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 //Some(Right(aaa)) 

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") //"Got error code 400" 

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] //Right[Int, String](a) 

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] //Some(a) 

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:

 //  ,   `pure`, //    import cats.implicits._ import cats.instances.option._ "a".pure[Option] //could not find implicit value for parameter F: cats.Applicative[Option] 

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.

Source: https://habr.com/ru/post/id448128/


All Articles