Opsi/Mungkin, Baik, dan Monad Masa Depan dalam JavaScript, Python, Ruby, Swift, dan Scala

Diterbitkan: 2022-03-11

Tutorial monad ini memberikan penjelasan singkat tentang monad dan menunjukkan cara mengimplementasikan monad yang paling berguna dalam lima bahasa pemrograman yang berbeda—jika Anda mencari monad di JavaScript, monad dengan Python, monad di Ruby, monad di Swift, dan/atau monad di Scala, atau untuk membandingkan implementasi apa pun, Anda membaca artikel yang tepat!

Dengan menggunakan monad ini, Anda akan menyingkirkan serangkaian bug seperti pengecualian null-pointer, pengecualian yang tidak ditangani, dan kondisi balapan.

Inilah yang saya ulas di bawah ini:

  • Pengantar teori kategori
  • Definisi monad
  • Implementasi monad Option (“Mungkin”), Either monad, dan Future monad, ditambah contoh program yang memanfaatkannya, dalam JavaScript, Python, Ruby, Swift, dan Scala

Mari kita mulai! Perhentian pertama kami adalah teori kategori, yang merupakan dasar untuk monad.

Pengantar Teori Kategori

Teori kategori adalah bidang matematika yang aktif dikembangkan pada pertengahan abad ke-20. Sekarang ini adalah dasar dari banyak konsep pemrograman fungsional termasuk monad. Mari kita lihat sekilas beberapa konsep teori kategori, yang disesuaikan dengan terminologi pengembangan perangkat lunak.

Jadi ada tiga konsep inti yang mendefinisikan kategori:

  1. Ketik sama seperti yang kita lihat dalam bahasa yang diketik secara statis. Contoh: Int , String , Dog , Cat , dll.
  2. Fungsi menghubungkan dua jenis. Oleh karena itu, mereka dapat direpresentasikan sebagai panah dari satu jenis ke jenis lain, atau untuk diri mereka sendiri. Fungsi $f$ dari tipe $T$ ke tipe $U$ dapat dinotasikan sebagai $f: T \ke U$. Anda dapat menganggapnya sebagai fungsi bahasa pemrograman yang mengambil argumen tipe $T$ dan mengembalikan nilai tipe $U$.
  3. Komposisi adalah operasi, dilambangkan dengan operator $\cdot$, yang membangun fungsi baru dari yang sudah ada. Dalam sebuah kategori, selalu dijamin untuk setiap fungsi $f: T \to U$ dan $g: U \to V$ terdapat fungsi unik $h: T \to V$. Fungsi ini dilambangkan sebagai $f \cdot g$. Operasi secara efektif memetakan sepasang fungsi ke fungsi lain. Dalam bahasa pemrograman, operasi ini, tentu saja, selalu memungkinkan. Misalnya, jika Anda memiliki fungsi yang mengembalikan panjang string —$strlen: String \to Int$—dan fungsi yang memberi tahu apakah angkanya genap —$even: Int \to Boolean$—maka Anda dapat membuat function $even{\_}strlen: String \to Boolean$ yang memberitahukan apakah panjang String genap. Dalam hal ini $even{\_}strlen = even \cdot strlen$. Komposisi menyiratkan dua fitur:
    1. Keterkaitan: $f \cdot g \cdot h = (f \cdot g) \cdot h = f \cdot (g \cdot h)$
    2. Adanya fungsi identitas: $\forall T: \exists f: T \to T$, atau dalam bahasa Inggris biasa, untuk setiap tipe $T$ terdapat fungsi yang memetakan $T$ ke dirinya sendiri.

Jadi mari kita lihat kategori sederhana.

Kategori sederhana yang melibatkan String, Int, dan Double, dan beberapa fungsi di antaranya.

Catatan tambahan: Kami berasumsi bahwa Int , String dan semua tipe lainnya di sini dijamin bukan nol, yaitu, nilai nol tidak ada.

Catatan samping 2: Ini sebenarnya hanya bagian dari sebuah kategori, tetapi hanya itu yang kami inginkan untuk diskusi kami, karena memiliki semua bagian penting yang kami butuhkan dan diagramnya tidak terlalu berantakan seperti ini. Kategori sebenarnya juga akan memiliki semua fungsi yang tersusun seperti $roundToString: Double \to String = intToString \cdot round$, untuk memenuhi klausa komposisi kategori.

Anda mungkin memperhatikan bahwa fungsi dalam kategori ini sangat sederhana. Bahkan hampir tidak mungkin untuk memiliki bug dalam fungsi-fungsi ini. Tidak ada nol, tidak ada pengecualian, hanya aritmatika dan bekerja dengan memori. Jadi, satu-satunya hal buruk yang dapat terjadi adalah kegagalan prosesor atau memori—dalam hal ini Anda tetap harus menghentikan program—tetapi itu jarang terjadi.

Bukankah lebih baik jika semua kode kita bekerja pada tingkat stabilitas ini? Sangat! Tapi bagaimana dengan I/O, misalnya? Kita pasti tidak bisa hidup tanpanya. Di sinilah solusi monad datang untuk menyelamatkan: Mereka mengisolasi semua operasi yang tidak stabil menjadi potongan kode yang sangat kecil dan diaudit dengan sangat baik—maka Anda dapat menggunakan perhitungan yang stabil di seluruh aplikasi Anda!

Masuk Monad

Sebut saja perilaku tidak stabil seperti I/O sebagai efek samping . Sekarang kita ingin dapat bekerja dengan semua fungsi yang kita definisikan sebelumnya seperti length dan tipe seperti String secara stabil dengan adanya efek samping ini.

Jadi mari kita mulai dengan kategori kosong $M[A]$ dan membuatnya menjadi kategori yang akan memiliki nilai dengan satu jenis efek samping tertentu dan juga nilai tanpa efek samping. Mari kita asumsikan kita telah mendefinisikan kategori ini dan itu kosong. Saat ini tidak ada yang berguna yang dapat kita lakukan dengannya, jadi untuk membuatnya berguna, kita akan mengikuti tiga langkah berikut:

  1. Isi dengan nilai tipe dari kategori $A$, seperti String , Int , Double , dll. (kotak hijau pada diagram di bawah)
  2. Setelah kita memiliki nilai-nilai ini, kita masih tidak dapat melakukan sesuatu yang berarti dengannya, jadi kita memerlukan cara untuk mengambil setiap fungsi $f: T \ke U$ dari $A$ dan membuat fungsi $g: M[T] \ke M [U]$ (panah biru pada diagram di bawah). Setelah kita memiliki fungsi-fungsi ini, kita dapat melakukan segalanya dengan nilai-nilai dalam kategori $M[A]$ yang dapat kita lakukan dalam kategori $A$.
  3. Sekarang kita memiliki kategori $M[A]$ baru, sebuah kelas fungsi baru muncul dengan tanda tangan $h: T \to M[U]$ (panah merah pada diagram di bawah). Mereka muncul sebagai hasil dari mempromosikan nilai-nilai pada langkah pertama sebagai bagian dari basis kode kami, yaitu, kami menulisnya sesuai kebutuhan; ini adalah hal utama yang akan membedakan bekerja dengan $M[A]$ versus bekerja dengan $A$. Langkah terakhir adalah membuat fungsi-fungsi ini bekerja dengan baik pada tipe di $M[A]$ juga, yaitu, mampu menurunkan fungsi $m: M[T] \ke M[U]$ dari $h: T \ ke M[U]$

Membuat kategori baru: Kategori A dan M[A], ditambah panah merah dari Double A ke Int M[A], berlabel "roundAsync". M[A] menggunakan kembali setiap nilai dan fungsi A pada titik ini.

Jadi mari kita mulai dengan mendefinisikan dua cara mempromosikan nilai tipe $A$ ke nilai tipe $M[A]$: satu fungsi tanpa efek samping dan satu lagi dengan efek samping.

  1. Yang pertama disebut $pure$ dan didefinisikan untuk setiap nilai dari kategori stabil: $pure: T \to M[T]$. Nilai $M[T]$ yang dihasilkan tidak akan memiliki efek samping, oleh karena itu fungsi ini disebut $pure$. Misalnya, untuk monad I/O, $pure$ akan segera mengembalikan beberapa nilai tanpa kemungkinan gagal.
  2. Yang kedua disebut $constructor$ dan, tidak seperti $pure$, mengembalikan $M[T]$ dengan beberapa efek samping. Contoh dari $constructor$ untuk monad I/O async dapat berupa fungsi yang mengambil beberapa data dari web dan mengembalikannya sebagai String . Nilai yang dikembalikan oleh $constructor$ akan memiliki tipe $M[String]$ dalam kasus ini.

Sekarang kita memiliki dua cara untuk mempromosikan nilai ke dalam $M[A]$, terserah Anda sebagai programmer untuk memilih fungsi mana yang akan digunakan, tergantung pada tujuan program Anda. Mari kita pertimbangkan sebuah contoh di sini: Anda ingin mengambil halaman HTML seperti https://www.toptal.com/javascript/option-maybe-either-future-monads-js dan untuk ini Anda membuat fungsi $fetch$. Karena apa pun bisa salah saat mengambilnya—pikirkan kegagalan jaringan, dll.—Anda akan menggunakan $M[String]$ sebagai tipe pengembalian dari fungsi ini. Jadi itu akan terlihat seperti $fetch: String \to M[String]$ dan di suatu tempat di badan fungsi di sana kita akan menggunakan $constructor$ untuk $M$.

Sekarang mari kita asumsikan kita membuat fungsi tiruan untuk pengujian: $fetchMock: String \to M[String]$. Itu masih memiliki tanda tangan yang sama, tetapi kali ini kami hanya menyuntikkan halaman HTML yang dihasilkan ke dalam tubuh $fetchMock$ tanpa melakukan operasi jaringan yang tidak stabil. Jadi dalam hal ini kita hanya menggunakan $pure$ dalam implementasi $fetchMock$.

Sebagai langkah selanjutnya, kita membutuhkan sebuah fungsi yang dengan aman mempromosikan fungsi sembarang $f$ dari kategori $A$ ke $M[A]$ (panah biru dalam diagram). Fungsi ini disebut $map: (T \ke U) \to (M[T] \to M[U])$.

Sekarang kita memiliki kategori (yang dapat memiliki efek samping jika kita menggunakan $constructor$), yang juga memiliki semua fungsi dari kategori stable, yang berarti mereka juga stabil di $M[A]$. Anda mungkin memperhatikan bahwa kami secara eksplisit memperkenalkan kelas fungsi lain seperti $f: T \to M[U]$. Misalnya, $pure$ dan $constructor$ adalah contoh dari fungsi tersebut untuk $U = T$, tapi jelas bisa lebih, seperti jika kita menggunakan $pure$ dan kemudian $map$. Jadi, secara umum, kita membutuhkan cara untuk menangani fungsi arbitrer dalam bentuk $f: T \to M[U]$.

Jika kita ingin membuat fungsi baru berdasarkan $f$ yang dapat diterapkan ke $M[T]$, kita dapat mencoba menggunakan $map$. Tapi itu akan membawa kita ke fungsi $g: M[T] \to M[M[U]]$, yang tidak baik karena kita tidak ingin memiliki satu kategori lagi $M[M[A]]$. Untuk mengatasi masalah ini, kami memperkenalkan satu fungsi terakhir: $flatMap: (T \to M[U]) \to (M[T] \to M[U])$.

Tapi mengapa kita ingin melakukan itu? Mari kita asumsikan kita setelah langkah 2, yaitu, kita memiliki $pure$, $constructor$, dan $map$. Katakanlah kita ingin mengambil halaman HTML dari toptal.com, lalu memindai semua URL di sana serta mengambilnya. Saya akan membuat fungsi $fetch: String \to M[String]$ yang mengambil hanya satu URL dan mengembalikan halaman HTML.

Kemudian saya akan menerapkan fungsi ini ke URL dan mendapatkan halaman dari toptal.com, yaitu $x: M[String]$. Sekarang, saya melakukan beberapa transformasi pada $x$ dan akhirnya sampai pada beberapa URL $u: M[String]$. Saya ingin menerapkan fungsi $fetch$, tetapi tidak bisa, karena dibutuhkan tipe $String$, bukan $M[String]$. Itulah mengapa kita membutuhkan $flatMap$ untuk mengonversi $fetch: String \to M[String]$ menjadi $m_fetch: M[String] \to M[String]$.

Sekarang setelah kita menyelesaikan ketiga langkah, kita sebenarnya dapat menyusun transformasi nilai apa pun yang kita butuhkan. Misalnya, jika Anda memiliki nilai $x$ dengan tipe $M[T]$ dan $f: T \ke U$, Anda dapat menggunakan $map$ untuk menerapkan $f$ ke nilai $x$ dan mendapatkan nilai $y$ bertipe $M[U]$. Dengan begitu, transformasi nilai apa pun dapat dilakukan dengan cara yang 100 persen bebas bug, selama implementasi $pure$, $constructor$, $map$ dan $flatMap$ bebas bug.

Jadi, alih-alih berurusan dengan beberapa efek buruk setiap kali Anda menemukannya di basis kode Anda, Anda hanya perlu memastikan bahwa hanya empat fungsi ini yang diterapkan dengan benar. Di akhir program, Anda hanya akan mendapatkan satu $M[X]$ di mana Anda dapat dengan aman membuka nilai $X$ dan menangani semua kasus kesalahan.

Inilah monad : sesuatu yang mengimplementasikan $pure$, $map$, dan $flatMap$. (Sebenarnya $map$ dapat diturunkan dari $pure$ dan $flatMap$, tetapi fungsinya sangat berguna dan tersebar luas, jadi saya tidak menghilangkannya dari definisi.)

The Option Monad, alias Mungkin Monad

Oke, mari selami implementasi praktis dan penggunaan monad. Monad pertama yang sangat membantu adalah Option monad. Jika Anda berasal dari bahasa pemrograman klasik, Anda mungkin mengalami banyak crash karena kesalahan penunjuk nol yang terkenal. Tony Hoare, penemu null, menyebut penemuan ini "The Billion Dollar Mistake":

Hal ini telah menyebabkan kesalahan yang tak terhitung banyaknya, kerentanan, dan sistem crash, yang mungkin telah menyebabkan satu miliar dolar rasa sakit dan kerusakan dalam empat puluh tahun terakhir.

Jadi mari kita coba untuk memperbaikinya. Opsi monad memiliki beberapa nilai bukan nol, atau tidak ada nilai. Sangat mirip dengan nilai nol, tetapi dengan monad ini, kita dapat dengan aman menggunakan fungsi yang terdefinisi dengan baik tanpa takut dengan pengecualian pointer nol. Mari kita lihat implementasi dalam berbagai bahasa:

JavaScript—Opsi Monad/Mungkin Monad

 class Monad { // pure :: a -> M a pure = () => { throw "pure method needs to be implemented" } // flatMap :: # M a -> (a -> M b) -> M b flatMap = (x) => { throw "flatMap method needs to be implemented" } // map :: # M a -> (a -> b) -> M b map = f => this.flatMap(x => new this.pure(f(x))) } export class Option extends Monad { // pure :: a -> Option a pure = (value) => { if ((value === null) || (value === undefined)) { return none; } return new Some(value) } // flatMap :: # Option a -> (a -> Option b) -> Option b flatMap = f => this.constructor.name === 'None' ? none : f(this.value) // equals :: # M a -> M a -> boolean equals = (x) => this.toString() === x.toString() } class None extends Option { toString() { return 'None'; } } // Cached None class value export const none = new None() Option.pure = none.pure export class Some extends Option { constructor(value) { super(); this.value = value; } toString() { return `Some(${this.value})` } }

Python—Opsi Monad/Mungkin Monad

 class Monad: # pure :: a -> M a @staticmethod def pure(x): raise Exception("pure method needs to be implemented") # flat_map :: # M a -> (a -> M b) -> M b def flat_map(self, f): raise Exception("flat_map method needs to be implemented") # map :: # M a -> (a -> b) -> M b def map(self, f): return self.flat_map(lambda x: self.pure(f(x))) class Option(Monad): # pure :: a -> Option a @staticmethod def pure(x): return Some(x) # flat_map :: # Option a -> (a -> Option b) -> Option b def flat_map(self, f): if self.defined: return f(self.value) else: return nil class Some(Option): def __init__(self, value): self.value = value self.defined = True class Nil(Option): def __init__(self): self.value = None self.defined = False nil = Nil()

Ruby—Opsi Monad/Mungkin Monad

 class Monad # pure :: a -> M a def self.pure(x) raise StandardError("pure method needs to be implemented") end # pure :: a -> M a def pure(x) self.class.pure(x) end def flat_map(f) raise StandardError("flat_map method needs to be implemented") end # map :: # M a -> (a -> b) -> M b def map(f) flat_map(-> (x) { pure(f.call(x)) }) end end class Option < Monad attr_accessor :defined, :value # pure :: a -> Option a def self.pure(x) Some.new(x) end # pure :: a -> Option a def pure(x) Some.new(x) end # flat_map :: # Option a -> (a -> Option b) -> Option b def flat_map(f) if defined f.call(value) else $none end end end class Some < Option def initialize(value) @value = value @defined = true end end class None < Option def initialize() @defined = false end end $none = None.new()

Swift—Opsi Monad/Mungkin Monad

 import Foundation enum Maybe<A> { case None case Some(A) static func pure<B>(_ value: B) -> Maybe<B> { return .Some(value) } func flatMap<B>(_ f: (A) -> Maybe<B>) -> Maybe<B> { switch self { case .None: return .None case .Some(let value): return f(value) } } func map<B>(f: (A) -> B) -> Maybe<B> { return self.flatMap { type(of: self).pure(f($0)) } } }

Scala—Opsi Monad/Mungkin Monad

 import language.higherKinds trait Monad[M[_]] { def pure[A](a: A): M[A] def flatMap[A, B](ma: M[A])(f: A => M[B]): M[B] def map[A, B](ma: M[A])(f: A => B): M[B] = flatMap(ma)(x => pure(f(x))) } object Monad { def apply[F[_]](implicit M: Monad[F]): Monad[F] = M implicit val myOptionMonad = new Monad[MyOption] { def pure[A](a: A) = MySome(a) def flatMap[A, B](ma: MyOption[A])(f: A => MyOption[B]): MyOption[B] = ma match { case MyNone => MyNone case MySome(a) => f(a) } } } sealed trait MyOption[+A] { def flatMap[B](f: A => MyOption[B]): MyOption[B] = Monad[MyOption].flatMap(this)(f) def map[B](f: A => B): MyOption[B] = Monad[MyOption].map(this)(f) } case object MyNone extends MyOption[Nothing] case class MySome[A](x: A) extends MyOption[A]

Kami memulai dengan mengimplementasikan kelas Monad yang akan menjadi dasar untuk semua implementasi monad kami. Memiliki kelas ini sangat berguna, karena dengan menerapkan hanya dua metodenya — pure dan flatMap —untuk monad tertentu, Anda akan mendapatkan banyak metode secara gratis (kami membatasinya hanya pada metode map dalam contoh kami, tetapi umumnya ada banyak metode metode berguna lainnya, seperti sequence dan traverse untuk bekerja dengan array Monad s).

Kita dapat mengekspresikan map sebagai komposisi pure dan flatMap . Anda dapat melihat dari tanda tangan flatMap $flatMap: (T \to M[U]) \to (M[T] \to M[U])$ bahwa itu sangat dekat dengan $map: (T \to U) \ ke (M[T] \ke M[U])$. Perbedaannya adalah $M$ tambahan di tengah, tetapi kita dapat menggunakan fungsi pure untuk mengubah $U$ menjadi $M[U]$. Dengan cara itu kami mengekspresikan map dalam bentuk flatMap dan pure .

Ini bekerja dengan baik untuk Scala, karena memiliki sistem tipe lanjutan. Ini juga bekerja dengan baik untuk JS, Python, dan Ruby, karena mereka diketik secara dinamis. Sayangnya, ini tidak berfungsi untuk Swift, karena diketik secara statis dan tidak memiliki fitur tipe lanjutan seperti tipe yang lebih tinggi, jadi untuk Swift kita harus mengimplementasikan map untuk setiap monad.

Perhatikan juga bahwa Option monad sudah menjadi standar de facto untuk bahasa seperti Swift dan Scala, jadi kami menggunakan nama yang sedikit berbeda untuk implementasi monad kami.

Sekarang setelah kita memiliki kelas dasar Monad , mari masuk ke implementasi monad Option kita. Seperti yang disebutkan sebelumnya, ide dasarnya adalah bahwa Option memiliki beberapa nilai (disebut Some ) atau atau tidak memiliki nilai sama sekali ( None ).

Metode pure hanya mempromosikan nilai ke Some , sedangkan metode flatMap memeriksa nilai Option saat ini — jika None maka ia mengembalikan None , dan jika Some dengan nilai yang mendasarinya, ia mengekstrak nilai yang mendasarinya, menerapkan f() ke itu dan mengembalikan hasilnya.

Perhatikan bahwa hanya dengan menggunakan dua fungsi dan map ini, tidak mungkin untuk masuk ke pengecualian pointer nol—selamanya. (Masalahnya berpotensi muncul dalam implementasi metode flatMap kami, tetapi itu hanya beberapa baris dalam kode kami yang kami periksa sekali. Setelah itu, kami hanya menggunakan implementasi Option monad kami di seluruh kode kami di ribuan tempat dan tidak harus takut pengecualian pointer nol sama sekali.)

Monad

Mari selami monad kedua: Entah. Ini pada dasarnya sama dengan monad Option, tetapi dengan Some disebut Right dan None disebut Left . Tapi kali ini, Left juga diperbolehkan memiliki nilai dasar.

Kami membutuhkan itu karena sangat nyaman untuk mengekspresikan melempar pengecualian. Jika pengecualian terjadi, maka nilai Either akan menjadi Left(Exception) . Fungsi flatMap tidak berkembang jika nilainya Left , yang mengulangi semantik melempar pengecualian: Jika pengecualian terjadi, kami menghentikan eksekusi lebih lanjut.

JavaScript—Entah Monad

 import Monad from './monad'; export class Either extends Monad { // pure :: a -> Either a pure = (value) => { return new Right(value) } // flatMap :: # Either a -> (a -> Either b) -> Either b flatMap = f => this.isLeft() ? this : f(this.value) isLeft = () => this.constructor.name === 'Left' } export class Left extends Either { constructor(value) { super(); this.value = value; } toString() { return `Left(${this.value})` } } export class Right extends Either { constructor(value) { super(); this.value = value; } toString() { return `Right(${this.value})` } } // attempt :: (() -> a) -> M a Either.attempt = f => { try { return new Right(f()) } catch(e) { return new Left(e) } } Either.pure = (new Left(null)).pure

Python—Entah Monad

 from monad import Monad class Either(Monad): # pure :: a -> Either a @staticmethod def pure(value): return Right(value) # flat_map :: # Either a -> (a -> Either b) -> Either b def flat_map(self, f): if self.is_left: return self else: return f(self.value) class Left(Either): def __init__(self, value): self.value = value self.is_left = True class Right(Either): def __init__(self, value): self.value = value self.is_left = False

Ruby—Entah Monad

 require_relative './monad' class Either < Monad attr_accessor :is_left, :value # pure :: a -> Either a def self.pure(value) Right.new(value) end # pure :: a -> Either a def pure(value) self.class.pure(value) end # flat_map :: # Either a -> (a -> Either b) -> Either b def flat_map(f) if is_left self else f.call(value) end end end class Left < Either def initialize(value) @value = value @is_left = true end end class Right < Either def initialize(value) @value = value @is_left = false end end

Swift—Entah Monad

 import Foundation enum Either<A, B> { case Left(A) case Right(B) static func pure<C>(_ value: C) -> Either<A, C> { return Either<A, C>.Right(value) } func flatMap<D>(_ f: (B) -> Either<A, D>) -> Either<A, D> { switch self { case .Left(let x): return Either<A, D>.Left(x) case .Right(let x): return f(x) } } func map<C>(f: (B) -> C) -> Either<A, C> { return self.flatMap { Either<A, C>.pure(f($0)) } } }

Scala—Entah Monad

 package monad sealed trait MyEither[+E, +A] { def flatMap[EE >: E, B](f: A => MyEither[EE, B]): MyEither[EE, B] = Monad[MyEither[EE, ?]].flatMap(this)(f) def map[EE >: E, B](f: A => B): MyEither[EE, B] = Monad[MyEither[EE, ?]].map(this)(f) } case class MyLeft[E](e: E) extends MyEither[E, Nothing] case class MyRight[A](a: A) extends MyEither[Nothing, A] // ... implicit def myEitherMonad[E] = new Monad[MyEither[E, ?]] { def pure[A](a: A) = MyRight(a) def flatMap[A, B](ma: MyEither[E, A])(f: A => MyEither[E, B]): MyEither[E, B] = ma match { case MyLeft(a) => MyLeft(a) case MyRight(b) => f(b) } }

Perhatikan juga bahwa mudah untuk menangkap pengecualian: Yang harus Anda lakukan adalah memetakan Left to Right . (Meskipun, kami tidak melakukannya dalam contoh kami, untuk singkatnya.)

Monad Masa Depan

Mari kita jelajahi monad terakhir yang kita butuhkan: monad masa depan. Monad Masa Depan pada dasarnya adalah wadah untuk nilai yang tersedia sekarang atau akan tersedia dalam waktu dekat. Anda dapat membuat rantai Futures dengan map dan flatMap yang akan menunggu nilai Future diselesaikan sebelum mengeksekusi potongan kode berikutnya yang bergantung pada nilai yang diselesaikan terlebih dahulu. Ini sangat mirip dengan konsep Janji di JS.

Tujuan desain kami sekarang adalah menjembatani API asinkron yang ada dalam berbagai bahasa ke satu basis yang konsisten. Ternyata pendekatan desain termudah adalah menggunakan panggilan balik di $constructor$.

Sementara desain panggilan balik memperkenalkan masalah panggilan balik neraka dalam JavaScript dan bahasa lain, itu tidak akan menjadi masalah bagi kami, karena kami menggunakan monads. Faktanya, objek Promise —dasar solusi JavaScript untuk panggilan balik neraka—adalah monad itu sendiri!

Bagaimana dengan konstruktor monad Masa Depan? Apakah memiliki tanda tangan ini:

constructor :: ((Either err a -> void) -> void) -> Future (Either err a)

Mari kita bagi menjadi beberapa bagian. Pertama, mari kita definisikan:

type Callback = Either err a -> void

Jadi Callback adalah fungsi yang mengambil kesalahan atau nilai yang diselesaikan sebagai argumen, dan tidak mengembalikan apa pun. Sekarang tanda tangan kami terlihat seperti ini:

constructor :: (Callback -> void) -> Future (Either err a)

Jadi kita perlu menyediakannya sebuah fungsi yang tidak mengembalikan apa-apa dan memicu panggilan balik segera setelah perhitungan async diselesaikan dengan kesalahan atau nilai tertentu. Terlihat cukup mudah untuk membuat jembatan untuk bahasa apa pun.

Adapun untuk desain monad Future sendiri, mari kita lihat struktur internalnya. Ide kuncinya adalah memiliki variabel cache yang menyimpan nilai jika monad Future diselesaikan, atau tidak menyimpan apa pun sebaliknya. Anda dapat berlangganan Masa Depan dengan panggilan balik yang akan segera dipicu jika nilai diselesaikan, atau jika tidak, akan memasukkan panggilan balik ke dalam daftar pelanggan.

Setelah Masa Depan diselesaikan, setiap panggilan balik dalam daftar ini akan dipicu tepat satu kali dengan nilai yang diselesaikan di utas terpisah (atau sebagai fungsi berikutnya yang akan dieksekusi dalam loop peristiwa, dalam kasus JS.) Perhatikan bahwa sangat penting untuk hati-hati gunakan primitif sinkronisasi, jika tidak, kondisi balapan dimungkinkan.

Alur dasarnya adalah: Anda memulai komputasi asinkron yang diberikan sebagai argumen konstruktor, dan mengarahkan panggilan baliknya ke metode panggilan balik internal kami. Sementara itu, Anda dapat berlangganan monad Future dan memasukkan panggilan balik Anda ke dalam antrean. Setelah perhitungan selesai, metode panggilan balik internal memanggil semua panggilan balik dalam antrian. Jika Anda terbiasa dengan Ekstensi Reaktif (RxJS, RxSwift, dll.), Mereka menggunakan pendekatan yang sangat mirip dengan penanganan async.

API publik dari monad Future terdiri dari pure , map , dan flatMap , seperti pada monad sebelumnya. Kami juga membutuhkan beberapa metode praktis:

  1. async , yang mengambil fungsi pemblokiran sinkron dan menjalankannya pada utas terpisah, dan
  2. traverse , yang mengambil larik nilai dan fungsi yang memetakan nilai ke Future , dan mengembalikan Future dari larik nilai yang diselesaikan

Mari kita lihat bagaimana hasilnya:

JavaScript—Monad Masa Depan

 import Monad from './monad'; import { Either, Left, Right } from './either'; import { none, Some } from './option'; export class Future extends Monad { // constructor :: ((Either err a -> void) -> void) -> Future (Either err a) constructor(f) { super(); this.subscribers = []; this.cache = none; f(this.callback) } // callback :: Either err a -> void callback = (value) => { this.cache = new Some(value) while (this.subscribers.length) { const subscriber = this.subscribers.shift(); subscriber(value) } } // subscribe :: (Either err a -> void) -> void subscribe = (subscriber) => (this.cache === none ? this.subscribers.push(subscriber) : subscriber(this.cache.value)) toPromise = () => new Promise( (resolve, reject) => this.subscribe(val => val.isLeft() ? reject(val.value) : resolve(val.value)) ) // pure :: a -> Future a pure = Future.pure // flatMap :: (a -> Future b) -> Future b flatMap = f => new Future( cb => this.subscribe(value => value.isLeft() ? cb(value) : f(value.value).subscribe(cb)) ) } Future.async = (nodeFunction, ...args) => { return new Future(cb => nodeFunction(...args, (err, data) => err ? cb(new Left(err)) : cb(new Right(data))) ); } Future.pure = value => new Future(cb => cb(Either.pure(value))) // traverse :: [a] -> (a -> Future b) -> Future [b] Future.traverse = list => f => list.reduce( (acc, elem) => acc.flatMap(values => f(elem).map(value => [...values, value])), Future.pure([]) )

Python—Monad . Masa Depan

 from monad import Monad from option import nil, Some from either import Either, Left, Right from functools import reduce import threading class Future(Monad): # __init__ :: ((Either err a -> void) -> void) -> Future (Either err a) def __init__(self, f): self.subscribers = [] self.cache = nil self.semaphore = threading.BoundedSemaphore(1) f(self.callback) # pure :: a -> Future a @staticmethod def pure(value): return Future(lambda cb: cb(Either.pure(value))) def exec(f, cb): try: data = f() cb(Right(data)) except Exception as err: cb(Left(err)) def exec_on_thread(f, cb): t = threading.Thread(target=Future.exec, args=[f, cb]) t.start() def async(f): return Future(lambda cb: Future.exec_on_thread(f, cb)) # flat_map :: (a -> Future b) -> Future b def flat_map(self, f): return Future( lambda cb: self.subscribe( lambda value: cb(value) if (value.is_left) else f(value.value).subscribe(cb) ) ) # traverse :: [a] -> (a -> Future b) -> Future [b] def traverse(arr): return lambda f: reduce( lambda acc, elem: acc.flat_map( lambda values: f(elem).map( lambda value: values + [value] ) ), arr, Future.pure([])) # callback :: Either err a -> void def callback(self, value): self.semaphore.acquire() self.cache = Some(value) while (len(self.subscribers) > 0): sub = self.subscribers.pop(0) t = threading.Thread(target=sub, args=[value]) t.start() self.semaphore.release() # subscribe :: (Either err a -> void) -> void def subscribe(self, subscriber): self.semaphore.acquire() if (self.cache.defined): self.semaphore.release() subscriber(self.cache.value) else: self.subscribers.append(subscriber) self.semaphore.release()

Ruby—Monad Masa Depan

 require_relative './monad' require_relative './either' require_relative './option' class Future < Monad attr_accessor :subscribers, :cache, :semaphore # initialize :: ((Either err a -> void) -> void) -> Future (Either err a) def initialize(f) @subscribers = [] @cache = $none @semaphore = Queue.new @semaphore.push(nil) f.call(method(:callback)) end # pure :: a -> Future a def self.pure(value) Future.new(-> (cb) { cb.call(Either.pure(value)) }) end def self.async(f, *args) Future.new(-> (cb) { Thread.new { begin cb.call(Right.new(f.call(*args))) rescue => e cb.call(Left.new(e)) end } }) end # pure :: a -> Future a def pure(value) self.class.pure(value) end # flat_map :: (a -> Future b) -> Future b def flat_map(f) Future.new(-> (cb) { subscribe(-> (value) { if (value.is_left) cb.call(value) else f.call(value.value).subscribe(cb) end }) }) end # traverse :: [a] -> (a -> Future b) -> Future [b] def self.traverse(arr, f) arr.reduce(Future.pure([])) do |acc, elem| acc.flat_map(-> (values) { f.call(elem).map(-> (value) { values + [value] }) }) end end # callback :: Either err a -> void def callback(value) semaphore.pop self.cache = Some.new(value) while (subscribers.count > 0) sub = self.subscribers.shift Thread.new { sub.call(value) } end semaphore.push(nil) end # subscribe :: (Either err a -> void) -> void def subscribe(subscriber) semaphore.pop if (self.cache.defined) semaphore.push(nil) subscriber.call(cache.value) else self.subscribers.push(subscriber) semaphore.push(nil) end end end

Swift—Monad Masa Depan

 import Foundation let background = DispatchQueue(label: "background", attributes: .concurrent) class Future<Err, A> { typealias Callback = (Either<Err, A>) -> Void var subscribers: Array<Callback> = Array<Callback>() var cache: Maybe<Either<Err, A>> = .None var semaphore = DispatchSemaphore(value: 1) lazy var callback: Callback = { value in self.semaphore.wait() self.cache = .Some(value) while (self.subscribers.count > 0) { let subscriber = self.subscribers.popLast() background.async { subscriber?(value) } } self.semaphore.signal() } init(_ f: @escaping (@escaping Callback) -> Void) { f(self.callback) } func subscribe(_ cb: @escaping Callback) { self.semaphore.wait() switch cache { case .None: subscribers.append(cb) self.semaphore.signal() case .Some(let value): self.semaphore.signal() cb(value) } } static func pure<B>(_ value: B) -> Future<Err, B> { return Future<Err, B> { $0(Either<Err, B>.pure(value)) } } func flatMap<B>(_ f: @escaping (A) -> Future<Err, B>) -> Future<Err, B> { return Future<Err, B> { [weak self] cb in guard let this = self else { return } this.subscribe { value in switch value { case .Left(let err): cb(Either<Err, B>.Left(err)) case .Right(let x): f(x).subscribe(cb) } } } } func map<B>(_ f: @escaping (A) -> B) -> Future<Err, B> { return self.flatMap { Future<Err, B>.pure(f($0)) } } static func traverse<B>(_ list: Array<A>, _ f: @escaping (A) -> Future<Err, B>) -> Future<Err, Array<B>> { return list.reduce(Future<Err, Array<B>>.pure(Array<B>())) { (acc: Future<Err, Array<B>>, elem: A) in return acc.flatMap { elems in return f(elem).map { val in return elems + [val] } } } } }

Scala—Monad . Masa Depan

 package monad import java.util.concurrent.Semaphore class MyFuture[A] { private var subscribers: List[MyEither[Exception, A] => Unit] = List() private var cache: MyOption[MyEither[Exception, A]] = MyNone private val semaphore = new Semaphore(1) def this(f: (MyEither[Exception, A] => Unit) => Unit) { this() f(this.callback _) } def flatMap[B](f: A => MyFuture[B]): MyFuture[B] = Monad[MyFuture].flatMap(this)(f) def map[B](f: A => B): MyFuture[B] = Monad[MyFuture].map(this)(f) def callback(value: MyEither[Exception, A]): Unit = { semaphore.acquire cache = MySome(value) subscribers.foreach { sub => val t = new Thread( new Runnable { def run: Unit = { sub(value) } } ) t.start } subscribers = List() semaphore.release } def subscribe(sub: MyEither[Exception, A] => Unit): Unit = { semaphore.acquire cache match { case MyNone => subscribers = sub :: subscribers semaphore.release case MySome(value) => semaphore.release sub(value) } } } object MyFuture { def async[B, C](f: B => C, arg: B): MyFuture[C] = new MyFuture[C]({ cb => val t = new Thread( new Runnable { def run: Unit = { try { cb(MyRight(f(arg))) } catch { case e: Exception => cb(MyLeft(e)) } } } ) t.start }) def traverse[A, B](list: List[A])(f: A => MyFuture[B]): MyFuture[List[B]] = { list.foldRight(Monad[MyFuture].pure(List[B]())) { (elem, acc) => Monad[MyFuture].flatMap(acc) ({ values => Monad[MyFuture].map(f(elem)) { value => value :: values } }) } } } // ... implicit val myFutureMonad = new Monad[MyFuture] { def pure[A](a: A): MyFuture[A] = new MyFuture[A]({ cb => cb(myEitherMonad[Exception].pure(a)) }) def flatMap[A, B](ma: MyFuture[A])(f: A => MyFuture[B]): MyFuture[B] = new MyFuture[B]({ cb => ma.subscribe(_ match { case MyLeft(e) => cb(MyLeft(e)) case MyRight(a) => f(a).subscribe(cb) }) }) }

Sekarang, perhatikan bagaimana API publik Future tidak berisi detail tingkat rendah seperti utas, semafor, atau hal-hal itu. Yang Anda butuhkan pada dasarnya adalah menyediakan sesuatu dengan panggilan balik, dan hanya itu!

Menyusun Program dari Monads

Oke, jadi mari kita coba menggunakan monad kita untuk membuat program yang sebenarnya. Katakanlah kami memiliki file dengan daftar URL dan kami ingin mengambil masing-masing URL ini secara paralel. Kemudian, kami ingin memotong respons masing-masing menjadi 200 byte untuk singkatnya dan mencetak hasilnya.

Kami memulai dengan mengonversi API bahasa yang ada ke antarmuka monadik (lihat fungsi readFile dan fetch ). Sekarang setelah kita memilikinya, kita bisa menyusunnya untuk mendapatkan hasil akhir sebagai satu rantai. Perhatikan bahwa rantai itu sendiri sangat aman, karena semua detail berdarah terkandung dalam monad.

JavaScript—Contoh Program Monad

 import { Future } from './future'; import { Either, Left, Right } from './either'; import { readFile } from 'fs'; import https from 'https'; const getResponse = url => new Future(cb => https.get(url, res => { var body = ''; res.on('data', data => body += data); res.on('end', data => cb(new Right(body))); res.on('error', err => cb(new Left(err))) })) const getShortResponse = url => getResponse(url).map(resp => resp.substring(0, 200)) Future .async(readFile, 'resources/urls.txt') .map(data => data.toString().split("\n")) .flatMap(urls => Future.traverse(urls)(getShortResponse)) .map(console.log)

Python—Sample Monad Program

 import http.client import threading import time import os from future import Future from either import Either, Left, Right conn = http.client.HTTPSConnection("en.wikipedia.org") def read_file_sync(uri): base_dir = os.path.dirname(__file__) #<-- absolute dir the script is in path = os.path.join(base_dir, uri) with open(path) as f: return f.read() def fetch_sync(uri): conn.request("GET", uri) r = conn.getresponse() return r.read().decode("utf-8")[:200] def read_file(uri): return Future.async(lambda: read_file_sync(uri)) def fetch(uri): return Future.async(lambda: fetch_sync(uri)) def main(args=None): lines = read_file("../resources/urls.txt").map(lambda res: res.splitlines()) content = lines.flat_map(lambda urls: Future.traverse(urls)(fetch)) output = content.map(lambda res: print("\n".join(res))) if __name__ == "__main__": main()

Ruby—Sample Monad Program

 require './lib/future' require 'net/http' require 'uri' semaphore = Queue.new def read(uri) Future.async(-> () { File.read(uri) }) end def fetch(url) Future.async(-> () { uri = URI(url) Net::HTTP.get_response(uri).body[0..200] }) end read("resources/urls.txt") .map(-> (x) { x.split("\n") }) .flat_map(-> (urls) { Future.traverse(urls, -> (url) { fetch(url) }) }) .map(-> (res) { puts res; semaphore.push(true) }) semaphore.pop

Swift—Sample Monad Program

 import Foundation enum Err: Error { case Some(String) } func readFile(_ path: String) -> Future<Error, String> { return Future<Error, String> { callback in background.async { let url = URL(fileURLWithPath: path) let text = try? String(contentsOf: url) if let res = text { callback(Either<Error, String>.pure(res)) } else { callback(Either<Error, String>.Left(Err.Some("Error reading urls.txt"))) } } } } func fetchUrl(_ url: String) -> Future<Error, String> { return Future<Error, String> { callback in background.async { let url = URL(string: url) let task = URLSession.shared.dataTask(with: url!) {(data, response, error) in if let err = error { callback(Either<Error, String>.Left(err)) return } guard let nonEmptyData = data else { callback(Either<Error, String>.Left(Err.Some("Empty response"))) return } guard let result = String(data: nonEmptyData, encoding: String.Encoding.utf8) else { callback(Either<Error, String>.Left(Err.Some("Cannot decode response"))) return } let index = result.index(result.startIndex, offsetBy: 200) callback(Either<Error, String>.pure(String(result[..<index]))) } task.resume() } } } var result: Any = "" let _ = readFile("\(projectDir)/Resources/urls.txt") .map { data -> [String] in data.components(separatedBy: "\n").filter { (line: String) in !line.isEmpty } }.flatMap { urls in return Future<Error, String>.traverse(urls) { url in return fetchUrl(url) } }.map { responses in print(responses) } RunLoop.main.run()

Scala—Sample Monad Program

 import scala.io.Source import java.util.concurrent.Semaphore import monad._ object Main extends App { val semaphore = new Semaphore(0) def readFile(name: String): MyFuture[List[String]] = MyFuture.async[String, List[String]](filename => Source.fromResource(filename).getLines.toList, name) def fetch(url: String): MyFuture[String] = MyFuture.async[String, String]( uri => Source.fromURL(uri).mkString.substring(0, 200), url ) val future = for { urls <- readFile("urls.txt") entries <- MyFuture.traverse(urls)(fetch _) } yield { println(entries) semaphore.release } semaphore.acquire }

There you have it—monad solutions in practice. You can find a repo containing all the code from this article on GitHub.

Overhead: Done. Benefits: Ongoing

For this simple monad-based program, it might look like overkill to use all the code that we wrote before. But that's just the initial setup, and it will stay constant in its size. Imagine that from now on, using monads, you can write a lot of async code, not worrying about threads, race conditions, semaphores, exceptions, or null pointers! Luar biasa!