JavaScript, Python, Ruby, Swift ve Scala'da Seçenek/Belki, İkisi ve Gelecek Monadlar

Yayınlanan: 2022-03-11

Bu monad öğreticisi, monad'ların kısa bir açıklamasını verir ve en kullanışlı olanların beş farklı programlama dilinde nasıl uygulanacağını gösterir - JavaScript'te monadlar, Python'da monadlar, Ruby'de monads, Swift'de monads ve/veya monads arıyorsanız Scala'da veya herhangi bir uygulamayı karşılaştırmak için doğru makaleyi okuyorsunuz!

Bu monadları kullanarak boş işaretçi istisnaları, işlenmeyen istisnalar ve yarış koşulları gibi bir dizi hatadan kurtulacaksınız.

Aşağıda ele aldığım şey bu:

  • Kategori teorisine giriş
  • bir monadın tanımı
  • Seçeneğin (“Belki”) monad, Ya monad ve Future monad'ın uygulamaları ve bunlara ek olarak JavaScript, Python, Ruby, Swift ve Scala'da bunlardan yararlanan örnek bir program

Başlayalım! İlk durağımız, monadların temeli olan kategori teorisidir.

Kategori Teorisine Giriş

Kategori teorisi, 20. yüzyılın ortalarında aktif olarak geliştirilen matematiksel bir alandır. Şimdi, monad da dahil olmak üzere birçok fonksiyonel programlama konseptinin temelidir. Yazılım geliştirme terminolojisine göre ayarlanmış bazı kategori teorisi kavramlarına hızlıca göz atalım.

Yani bir kategoriyi tanımlayan üç temel kavram vardır:

  1. Tip , statik olarak yazılan dillerde gördüğümüz gibidir. Örnekler: Int , String , Dog , Cat , vb.
  2. Fonksiyonlar iki türü birbirine bağlar. Bu nedenle, bir türden başka bir türe veya kendilerine bir ok olarak temsil edilebilirler. $T$ türünden $U$ türüne kadar olan $f$ işlevi $f: T \'den U$'a şöyle gösterilebilir. Bunu, $T$ türünde bir argüman alan ve $U$ türünde bir değer döndüren bir programlama dili işlevi olarak düşünebilirsiniz.
  3. Kompozisyon , $\cdot$ operatörü tarafından belirtilen, mevcut fonksiyonlardan yeni fonksiyonlar oluşturan bir işlemdir. Bir kategoride, $f: T \to U$ ve $g: U \to V$ işlevleri için her zaman garanti edilir. $h: T \to V$ adlı benzersiz bir işlev vardır. Bu fonksiyon $f \cdot g$ olarak gösterilir. İşlem, bir çift işlevi başka bir işleve etkin bir şekilde eşler. Programlama dillerinde bu işlem elbette her zaman mümkündür. Örneğin, bir dizgenin —$strlen: String \to Int$— uzunluğunu döndüren bir fonksiyonunuz ve sayının çift olup olmadığını söyleyen —$even: Int \to Boolean$— bir fonksiyonunuz varsa, o zaman bir function $even{\_}strlen: String uzunluğunun çift olup olmadığını söyleyen String \to Boolean$. Bu durumda $çift{\_}strlen = çift \cdot strlen$. Kompozisyon iki özelliği ima eder:
    1. İlişkilendirme: $f \cdot g \cdot h = (f \cdot g) \cdot h = f \cdot (g \cdot h)$
    2. Bir kimlik fonksiyonunun mevcudiyeti: $\forall T: \vardır f: T \to T$ veya düz İngilizce, her $T$ tipi için $T$'ı kendisine eşleyen bir fonksiyon vardır.

Şimdi basit bir kategoriye bakalım.

String, Int ve Double ve aralarındaki bazı işlevleri içeren basit bir kategori.

Yan not: Buradaki Int , String ve diğer tüm türlerin boş olmadığının garanti edildiğini varsayıyoruz, yani boş değer mevcut değil.

Yan not 2: Bu aslında bir kategorinin yalnızca bir parçası, ancak tartışmamız için tek istediğimiz bu, çünkü ihtiyacımız olan tüm temel parçalara sahip ve diyagram bu şekilde daha az karmaşık. Gerçek kategori ayrıca, kategorilerin kompozisyon yan tümcesini karşılamak için $roundToString: Double \to String = intToString \cdot round$ gibi tüm oluşturulmuş işlevlere sahip olacaktır.

Bu kategorideki işlevlerin çok basit olduğunu fark edebilirsiniz. Aslında bu fonksiyonlarda bug olması neredeyse imkansız. Boş değer yok, istisna yok, sadece aritmetik ve bellekle çalışma. Bu nedenle olabilecek tek kötü şey işlemci veya bellek arızasıdır - bu durumda programı yine de çökertmeniz gerekir - ancak bu çok nadiren olur.

Tüm kodlarımız bu kararlılık düzeyinde çalışsa iyi olmaz mıydı? Kesinlikle! Ama örneğin G/Ç ne olacak? Kesinlikle onsuz yaşayamayız. İşte monad çözümlerinin kurtarmaya geldiği yer: Tüm kararsız işlemleri süper küçük ve çok iyi denetlenmiş kod parçalarına ayırırlar - o zaman tüm uygulamanızda kararlı hesaplamalar kullanabilirsiniz!

Monadları Girin

G/Ç gibi kararsız davranışlara yan etki diyelim. Artık bu yan etkinin varlığında daha önce tanımladığımız length ve String gibi türler gibi tüm fonksiyonlarımız ile kararlı bir şekilde çalışabilmek istiyoruz.

Öyleyse boş bir kategori $M[A]$ ile başlayalım ve onu belirli bir yan etkiye sahip değerlere ve ayrıca yan etkisi olmayan değerlere sahip olacak bir kategori haline getirelim. Bu kategoriyi tanımladığımızı ve boş olduğunu varsayalım. Şu anda onunla yapabileceğimiz yararlı bir şey yok, bu yüzden onu kullanışlı hale getirmek için şu üç adımı izleyeceğiz:

  1. String , Int , Double vb. gibi $A$ kategorisindeki türlerin değerleriyle doldurun (aşağıdaki şemada yeşil kutular)
  2. Bu değerlere sahip olduğumuzda, yine de onlarla anlamlı bir şey yapamayız, bu nedenle $f: T \to U$ işlevlerinin her birini $A$'dan almanın ve bir $g işlevi yaratmanın bir yoluna ihtiyacımız var: M[T] \to M [U]$ (aşağıdaki şemada mavi oklar). Bu fonksiyonlara sahip olduğumuzda, $A$ kategorisinde yapabildiğimiz her şeyi $M[A]$ kategorisindeki değerlerle yapabiliriz.
  3. Artık yepyeni bir $M[A]$ kategorimiz olduğuna göre, $h: T \to M[U]$ imzalı yeni bir işlev sınıfı ortaya çıkıyor (aşağıdaki şemada kırmızı oklar). Kod tabanımızın bir parçası olarak birinci adımda değerleri teşvik etmenin bir sonucu olarak ortaya çıkarlar, yani bunları gerektiği gibi yazarız; bunlar $M[A]$ ile çalışmayı $A$ ile çalışmaktan ayıran başlıca şeylerdir. Son adım, bu işlevlerin $M[A]$ içindeki türlerde de iyi çalışmasını sağlamak, yani $m: M[T] \to M[U]$ işlevini $h: T \'den türetebilmek olacaktır. M[U]$'a

Yeni bir kategori oluşturma: A ve M[A] Kategorileri, artı A'nın Double'sinden M[A]'nın Int'sine, "roundAsync" etiketli kırmızı bir ok. M[A], bu noktada A'nın her değerini ve işlevini yeniden kullanır.

Öyleyse, $A$ türlerinin değerlerini $M[A]$ türlerinin değerlerine yükseltmenin iki yolunu tanımlayarak başlayalım: yan etkileri olmayan bir işlev ve yan etkileri olan bir işlev.

  1. Birincisi $pure$ olarak adlandırılır ve kararlı bir kategorinin her değeri için tanımlanır: $pure: T \to M[T]$. Ortaya çıkan $M[T]$ değerlerinin herhangi bir yan etkisi olmayacaktır, bu nedenle bu fonksiyona $pure$ adı verilir. Örneğin, bir G/Ç monad için $pure$, başarısızlık olasılığı olmadan hemen bir değer döndürür.
  2. İkincisine $constructor$ adı verilir ve $pure$'dan farklı olarak bazı yan etkilerle birlikte $M[T]$ döndürür. Eşzamansız bir G/Ç monad için böyle bir $constructor$ örneği, web'den bazı verileri alan ve onu bir String olarak döndüren bir işlev olabilir. Bu durumda $constructor$ tarafından döndürülen değerin türü $M[String]$ olacaktır.

Artık değerleri $M[A]$'a yükseltmenin iki yolu olduğuna göre, program hedeflerinize bağlı olarak hangi işlevi kullanacağınızı seçmek bir programcı olarak size kalmış. Burada bir örnek düşünelim: https://www.toptal.com/javascript/option-maybe-either-future-monads-js gibi bir HTML sayfası getirmek istiyorsunuz ve bunun için $fetch$ işlevini yapıyorsunuz. Getirirken herhangi bir şey ters gidebileceği için (ağ arızalarını vb. düşünün) bu işlevin dönüş türü olarak $M[String]$ kullanacaksınız. Böylece $fetch: String \to M[String]$ gibi bir şey görünecek ve fonksiyon gövdesinde bir yerde $M$ için $constructor$ kullanacağız.

Şimdi, test için sahte bir işlev yaptığımızı varsayalım: $fetchMock: String \to M[String]$. Hala aynı imzaya sahip, ancak bu sefer ortaya çıkan HTML sayfasını herhangi bir kararsız ağ işlemi yapmadan $fetchMock$ gövdesine enjekte ediyoruz. Bu durumda, $fetchMock$ uygulamasında sadece $pure$ kullanıyoruz.

Bir sonraki adım olarak, $A$ kategorisinden $M[A]$'a (şemadaki mavi oklar) herhangi bir $f$ fonksiyonunu güvenle destekleyen bir fonksiyona ihtiyacımız var. Bu işleve $map: (T \to U) \to (M[T] \to M[U])$ adı verilir.

Şimdi bir kategorimiz var (eğer $constructor$ kullanırsak yan etkileri olabilir), bu da kararlı kategorideki tüm fonksiyonlara sahiptir, yani bunlar $M[A]$ içinde de kararlıdır. $f: T \to M[U]$ gibi başka bir işlev sınıfını açıkça tanıttığımızı fark edebilirsiniz. Örneğin, $pure$ ve $constructor$, $U = T$ için bu tür işlevlerin örnekleridir, ancak açıkçası daha fazla olabilir, örneğin $pure$ ve ardından $map$ kullansaydık. Bu nedenle, genel olarak, $f: T \to M[U]$ biçimindeki keyfi işlevlerle başa çıkmak için bir yola ihtiyacımız var.

$M[T]$'a uygulanabilecek $f$ tabanlı yeni bir işlev yapmak istiyorsak, $map$ kullanmayı deneyebiliriz. Ama bu bizi $g: M[T] \to M[M[U]]$ işlevine getirecektir, ki bu iyi değil çünkü bir tane daha $M[M[A]]$ kategorisine sahip olmak istemiyoruz. Bu sorunla başa çıkmak için son bir işlev sunuyoruz: $flatMap: (T \to M[U]) \to (M[T] \to M[U])$.

Ama neden bunu yapmak isteyelim ki? 2. adımdan sonra olduğumuzu varsayalım, yani elimizde $pure$, $constructor$ ve $map$ var. Diyelim ki toptal.com'dan bir HTML sayfası almak istiyoruz, ardından oradaki tüm URL'leri tarayın ve alın. Yalnızca bir URL getiren ve bir HTML sayfası döndüren bir $fetch: String \to M[String]$ işlevi yapardım.

Sonra bu işlevi bir URL'ye uygular ve toptal.com'dan $x: M[String]$ olan bir sayfa alırdım. Şimdi, $x$ üzerinde biraz dönüşüm yapıyorum ve sonunda bir URL $u: M[String]$'a ulaştım. Buna $fetch$ işlevini uygulamak istiyorum, ancak yapamıyorum, çünkü $M[String]$ değil $String$ türünü alıyor. Bu nedenle, $fetch: String \to M[String]$ öğesini $m_fetch: M[String] \to M[String]$'a dönüştürmek için $flatMap$'a ihtiyacımız var.

Artık üç adımı da tamamladığımıza göre, gerçekten ihtiyacımız olan herhangi bir değer dönüşümünü oluşturabiliriz. Örneğin, $M[T]$ türünde $x$ değeriniz varsa ve $f: T \to U$'a sahipseniz, $f$'ı $x$ değerine uygulamak ve $y$ değerini almak için $map$ kullanabilirsiniz. $M[U]$ türünde. Bu şekilde, $pure$, $constructor$, $map$ ve $flatMap$ uygulamaları hatasız olduğu sürece, değerlerin herhangi bir dönüşümü yüzde 100 hatasız bir şekilde yapılabilir.

Bu nedenle, kod tabanınızda her karşılaştığınızda bazı kötü etkilerle uğraşmak yerine, yalnızca bu dört işlevin doğru şekilde uygulandığından emin olmanız gerekir. Programın sonunda, $X$ değerini güvenle açabileceğiniz ve tüm hata durumlarını halledebileceğiniz yalnızca bir $M[X]$ alacaksınız.

Bir monad budur: $pure$, $map$ ve $flatMap$ uygulayan bir şeydir. (Aslında $map$, $pure$ ve $flatMap$'dan türetilebilir, ancak çok kullanışlı ve yaygın bir işlevdir, bu yüzden onu tanımdan çıkarmadım.)

Seçenek Monad, diğer adıyla Belki Monad

Tamam, hadi monadların pratik uygulamasına ve kullanımına geçelim. Gerçekten yararlı olan ilk monad, Option monad'dır. Klasik programlama dillerinden geliyorsanız, kötü şöhretli boş gösterici hatası nedeniyle muhtemelen birçok çökmeyle karşılaşmışsınızdır. Null'un mucidi Tony Hoare, bu buluşu "Milyar Dolarlık Hata" olarak adlandırıyor:

Bu, son kırk yılda muhtemelen milyarlarca dolarlık acıya ve hasara neden olan sayısız hataya, güvenlik açığına ve sistem çökmesine neden oldu.

Öyleyse bunu geliştirmeye çalışalım. Option monad, ya boş olmayan bir değer tutar ya da değer içermez. Boş bir değere oldukça benzer, ancak bu monad'a sahip olarak, boş gösterici istisnasından korkmadan iyi tanımlanmış fonksiyonlarımızı güvenle kullanabiliriz. Farklı dillerdeki uygulamalara bir göz atalım:

JavaScript—Monad Seçeneği/Belki Monad Seçeneği

 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—Seçenek Monad/Belki 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—Seçenek Monad/Belki 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—Seçenek Monad/Belki 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—Seçenek Monad/Belki 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]

Tüm monad uygulamalarımızın temeli olacak bir Monad sınıfı uygulayarak başlıyoruz. Bu sınıfa sahip olmak çok kullanışlıdır, çünkü belirli bir monad için yöntemlerinden yalnızca ikisini ( pure ve flatMap ) uygulayarak birçok yöntemi ücretsiz olarak elde edersiniz (bunları örneklerimizde yalnızca map yöntemiyle sınırlandırıyoruz, ancak genellikle birçok yöntem vardır. Monad s dizileriyle çalışmak için sequence ve traverse gibi diğer faydalı yöntemler.

Haritayı pure ve flatMap map bileşimi olarak ifade edebiliriz. flatMap imzası olan $flatMap: (T \to M[U]) \to (M[T] \to M[U])$'dan bunun gerçekten $map'a yakın olduğunu görebilirsiniz: (T \to U) \ için (M[T] \to M[U])$. Aradaki fark, ortadaki ek $M$'dır, ancak $U$'ı $M[U]$'a dönüştürmek için pure işlevi kullanabiliriz. Bu şekilde map flatMap ve pure olarak ifade ederiz.

Bu, gelişmiş bir tip sistemine sahip olduğu için Scala için iyi çalışır. Dinamik olarak yazıldıkları için JS, Python ve Ruby için de iyi çalışır. Ne yazık ki Swift için çalışmıyor, çünkü statik olarak yazılmış ve daha yüksek türler gibi gelişmiş tip özelliklerine sahip değil, bu yüzden Swift için her monad için map uygulamamız gerekecek.

Ayrıca Option monad'ın Swift ve Scala gibi diller için fiili bir standart olduğunu da unutmayın, bu nedenle monad uygulamalarımız için biraz farklı adlar kullanıyoruz.

Artık bir temel Monad sınıfımız olduğuna göre, Option monad uygulamalarımıza geçelim. Daha önce bahsedildiği gibi, temel fikir şudur: Seçenek ya bir değer tutar ( Some olarak adlandırılır) ya da hiç değer tutmaz ( None ).

pure yöntemi yalnızca Some değerine bir değeri yükseltirken, flatMap yöntemi Option geçerli değerini kontrol eder - None ise None döndürür ve Some ise temel bir değere sahipse, temel değeri çıkarır, f() öğesini uygular. bu ve bir sonuç döndürür.

Yalnızca bu iki işlevi ve map kullanarak bir boş gösterici istisnasına girmenin asla mümkün olmadığını unutmayın. (Sorun, flatMap yöntemini uygulamamızda potansiyel olarak ortaya çıkabilir, ancak bu, kodumuzda bir kez kontrol ettiğimiz yalnızca birkaç satırdır. Ondan sonra, kodumuz boyunca Option monad uygulamamızı binlerce yerde kullanırız ve kullanmazız. boş gösterici istisnasından hiç korkmanız gerekir.)

Ya Monad

İkinci monad'a geçelim: Ya. Bu temelde Option monad ile aynıdır, ancak Some ile Right ve None olarak adlandırılır Left . Ancak bu sefer, Left da temel bir değere sahip olmasına izin verilir.

Buna ihtiyacımız var çünkü bir istisna atmayı ifade etmek çok uygun. Bir istisna meydana gelirse, Either değeri Left(Exception) olacaktır. flatMap işlevi, değer Left ise, istisna atma anlamını tekrarlayan ilerlemez: Bir istisna olursa, daha fazla yürütmeyi durdururuz.

JavaScript—Ya 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—Ya 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—Ya 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—Ya 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—Ya 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) } }

Ayrıca istisnaları yakalamanın kolay olduğunu da unutmayın: Tek yapmanız gereken Left Right eşlemek. (Ancak, kısa olması için örneklerimizde yapmıyoruz.)

Gelecek Monad

İhtiyacımız olan son monad'ı keşfedelim: Gelecek monad. Future monad, temelde şu anda mevcut olan veya yakın gelecekte mevcut olacak bir değer için bir kapsayıcıdır. İlk çözümlenen değere bağlı olan bir sonraki kod parçasını yürütmeden önce Future değerinin çözümlenmesini bekleyecek map ve flatMap ile Futures zincirleri oluşturabilirsiniz. Bu, JS'deki Sözler kavramına çok benzer.

Şimdiki tasarım hedefimiz, farklı dillerdeki mevcut zaman uyumsuz API'leri tek bir tutarlı tabana bağlamaktır. En kolay tasarım yaklaşımının $constructor$ içindeki geri aramaları kullanmak olduğu ortaya çıktı.

Geri arama tasarımı JavaScript ve diğer dillerde geri arama cehennemi sorununu ortaya çıkarırken, monad kullandığımız için bu bizim için sorun olmayacaktır. Aslında, JavaScript'in cehennemi geri çağırma çözümünün temeli olan Promise nesnesi bir monadın kendisidir!

Peki ya Future monad'ın kurucusu? Bu imza var mı:

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

Parçalara ayıralım. Önce tanımlayalım:

type Callback = Either err a -> void

Callback , argüman olarak ya bir hata ya da çözülmüş bir değer alan ve hiçbir şey döndürmeyen bir fonksiyondur. Şimdi imzamız şuna benziyor:

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

Bu nedenle, zaman uyumsuz hesaplama bir hataya veya bir değere çözülür çözülmez, hiçbir şey döndürmeyen ve bir geri aramayı tetikleyen bir işlev sağlamamız gerekir. Herhangi bir dil için bir köprü oluşturacak kadar kolay görünüyor.

Future monad'ın tasarımına gelince, onun iç yapısına bakalım. Ana fikir, Future monad çözülürse bir değeri tutan veya başka türlü hiçbir şeyi tutan bir önbellek değişkenine sahip olmaktır. Değer çözülürse hemen tetiklenecek veya çözülmezse geri aramayı abone listesine koyacak bir geri arama ile Geleceğe abone olabilirsiniz.

Gelecek çözümlendiğinde, bu listedeki her geri arama, çözülen değer ayrı bir iş parçacığında (veya JS durumunda olay döngüsünde yürütülecek bir sonraki işlev olarak) tam olarak bir kez tetiklenir. senkronizasyon ilkellerini dikkatli kullanın, aksi takdirde yarış koşulları mümkündür.

Temel akış şudur: Yapıcı argümanı olarak sağlanan zaman uyumsuz hesaplamayı başlatır ve geri çağrısını dahili geri çağırma yöntemimize yönlendirirsiniz. Bu arada Future monad'a abone olabilir ve geri aramalarınızı kuyruğa alabilirsiniz. Hesaplama yapıldıktan sonra, dahili geri arama yöntemi, kuyruktaki tüm geri aramaları çağırır. Reaktif Uzantılara (RxJS, RxSwift, vb.) aşina iseniz, zaman uyumsuz işlemeye çok benzer bir yaklaşım kullanırlar.

Future monad'ın genel API'si, önceki monadlarda olduğu gibi pure , map ve flatMap oluşur. Ayrıca birkaç kullanışlı yönteme ihtiyacımız olacak:

  1. zaman uyumlu bir engelleme işlevi alan ve bunu ayrı bir iş parçacığında yürüten async , ve
  2. Bir değer dizisini alan ve bir değeri bir Future ile eşleyen ve bir çözümlenmiş değerler dizisinin bir Future döndüren traverse

Bakalım nasıl sonuçlanacak:

JavaScript—Geleceğin Monad'ı

 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—Geleceğin Monad'ı

 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—Geleceğin Monad'ı

 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—Geleceğin Monad'ı

 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—Geleceğin Monad

 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) }) }) }

Şimdi, Future genel API'sinin diziler, semaforlar veya benzeri şeyler gibi düşük seviyeli ayrıntıları nasıl içermediğine dikkat edin. Tek ihtiyacınız olan, temelde bir geri arama ile bir şey sağlamak ve bu kadar!

Monads'tan Program Oluşturma

Tamam, gerçek bir program yapmak için monadlarımızı kullanmayı deneyelim. Bir URL listesi içeren bir dosyamız olduğunu ve bu URL'lerin her birini paralel olarak getirmek istediğimizi varsayalım. Ardından, kısalık olması için yanıtları her biri 200 bayta kesmek ve sonucu yazdırmak istiyoruz.

Mevcut dil API'lerini monadik arabirimlere dönüştürerek başlıyoruz ( readFile ve fetch işlevlerine bakın). Artık buna sahip olduğumuza göre, nihai sonucu tek bir zincir olarak elde etmek için onları bir araya getirebiliriz. Tüm kanlı ayrıntılar monadlarda bulunduğundan, zincirin kendisinin süper güvenli olduğunu unutmayın.

JavaScript—Örnek Monad Programı

 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! Mükemmel!