)) } } }

Scala - možnost Monada / Monada

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]

Začnemo z izvajanjem Monad razred, ki bo osnova za vse naše izvedbe monad. Imeti ta razred je zelo priročen, saj z izvajanjem le dveh njegovih metod - pure in flatMap —za določeno monado boste dobili veliko metod brezplačno (v naših primerih jih omejujemo zgolj na map metodo, na splošno pa obstaja veliko drugih uporabnih metod, na primer sequence in traverse za delo z nizi Monad s).

Lahko izrazimo map kot sestava pure in flatMap. Iz podpisa flatMap $ flatMap lahko vidite: (T do M [U]) do (M [T] do M [U]) $, da je res blizu $ map: (T do U) do (M [T] do M [U]) $. Razlika je v dodatnih $ M $ na sredini, vendar lahko uporabimo pure funkcija za pretvorbo $ U $ v $ M [U] $. Tako izrazimo map v smislu flatMap in pure.

To dobro deluje za Scalo, ker ima napreden sistem. Dobro deluje tudi za JS, Python in Ruby, ker so dinamično vneseni. Na žalost za Swift ne deluje, ker je statično natipkan in nima naprednih funkcij tipa, kot je tipi višje vrste , zato bomo za Swift morali uvesti map za vsako monado.

Upoštevajte tudi, da je monada Option že de facto standard za jezike, kot sta Swift in Scala, zato za izvedbe monad uporabljamo nekoliko drugačna imena.

Zdaj, ko imamo osnovo Monad razreda, pojdimo na naše izvedbe monade Option. Kot smo že omenili, je osnovna ideja, da ima možnost ali določeno vrednost (imenovano Some) ali pa nima nobene vrednosti (None).

The pure metoda preprosto poviša vrednost na Some, medtem ko flatMap metoda preverja trenutno vrednost Option - če je None potem se vrne None in če je Some z osnovno vrednostjo izvleče osnovno vrednost in uporabi f() in vrne rezultat.

Upoštevajte, da samo z uporabo teh dveh funkcij in map ni mogoče nikoli priti do izjeme za ničelni kazalec. (Težava lahko lahko nastanejo pri izvajanju flatMap metoda, vendar je to le nekaj vrstic v naši kodi, ki jih preverimo enkrat. Po tem samo uporabimo svojo izvedbo monade Option v celotni kodi na tisoče krajev in se nam sploh ni treba bati izjeme ničelnega kazalca.)

Monada bodisi

Potopimo se v drugo monado: Ali. To je v bistvu enako monadi Option, vendar z Some se imenuje Right in None se imenuje Left. Tokrat pa, Left dovoljeno imeti tudi osnovno vrednost.

To potrebujemo, ker je zelo priročno izraziti metanje izjeme. Če je prišlo do izjeme, potem je vrednost Either bo Left(Exception). The flatMap funkcija ne napreduje, če je vrednost Left, kar ponavlja semantiko metanja izjem: Če se je zgodila izjema, ustavimo nadaljnje izvajanje.

JavaScript - bodisi 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 - bodisi 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 - bodisi Monad

require_relative './monad' class Either 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

Hitri - bodisi Monad

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

Možnost / Mogoče, bodisi prihodnje monade v JavaScript, Python, Ruby, Swift in Scala

Ta vadnica za monade vsebuje kratko razlago monad in kaže, kako uporabiti najbolj uporabne v petih različnih programskih jezikih - če iščete monade v JavaScript , monade v Python , monade v Ruby , monade v Hitro , in / ali monade v Lestev ali če želite primerjati izvedbe, berete pravi članek!

Z uporabo teh monad se boste znebili vrste napak, kot so izjeme za ničelne kazalce, neobdelane izjeme in dirkalni pogoji.

To pokrivam spodaj:



  • Uvod v teorijo kategorij
  • Opredelitev monade
  • Implementacije monade Option (»Mogoče«), bodisi monade, bodisi monade Future ter vzorčni program, ki jih izkorišča, v JavaScript, Python, Ruby, Swift in Scala

Začnimo! Naša prva postaja je teorija kategorij, ki je osnova za monade.

Uvod v teorijo kategorij

Teorija kategorij je matematično področje, ki se je aktivno razvijalo sredi 20. stoletja. Zdaj je osnova številnih konceptov funkcionalnega programiranja, vključno z monado. Oglejmo si na hitro nekaj konceptov teorije kategorij, prilagojenih terminologiji razvoja programske opreme.

Torej obstajajo trije temeljni koncepti, ki opredeljujejo a kategorija:

  1. Tip je takšen, kot ga vidimo v statično natipkanih jezikih. Primeri: Int, String, Dog, Cat itd.
  2. Funkcije povežite dve vrsti. Zato jih lahko predstavimo kot puščico od ene do druge vrste ali pa sami. Funkcijo $ f $ od tipa $ T $ do tipa $ U $ lahko označimo kot $ f: T do U $. Lahko si predstavljate funkcijo programskega jezika, ki sprejme argument tipa $ T $ in vrne vrednost tipa $ U $.
  3. Sestava je operacija, označena z operaterjem $ cdot $, ki gradi nove funkcije iz obstoječih. V kategoriji je vedno zagotovljeno, da za vse funkcije $ f: T to U $ in $ g: U to V $ obstaja edinstvena funkcija $ h: T to V $. Ta funkcija je označena kot $ f cdot g $. Operacija učinkovito preslika par funkcij v drugo funkcijo. V programskih jezikih je ta operacija seveda vedno mogoča. Če imate na primer funkcijo, ki vrne dolžino niza - $ strlen: String to Int $ - in funkcijo, ki pove, ali je številka sodo - $ even: Int to Boolean $ - potem lahko naredite funkcija $ even { _} strlen: String v logično vrednost $, ki pove, ali je dolžina String je celo. V tem primeru je $ even { _} strlen = even cdot strlen $. Sestava vključuje dve značilnosti:
    1. Asocijativnost: $ f cdot g cdot h = (f cdot g) cdot h = f cdot (g cdot h) $
    2. Obstoj funkcije identitete: $ forall T: obstaja f: T do T $ ali v preprosti angleščini za vsak tip $ T $ obstaja funkcija, ki preslika $ T $ nase.

Poglejmo si torej preprosto kategorijo.

Preprosta kategorija, ki vključuje String, Int in Double, in nekatere funkcije med njimi.

Opomba: Predpostavljamo, da Int, String in za vse druge vrste tukaj je zajamčeno, da niso nič, to pomeni, da vrednost null ne obstaja.

Opomba 2: To je pravzaprav samo del kategorije, toda to je vse, kar želimo za našo razpravo, saj ima vse bistvene dele, ki jih potrebujemo, in diagram je na ta način manj natrpan. Prava kategorija bi imela tudi vse sestavljene funkcije, kot je $ roundToString: Double to String = intToString cdot round $, da izpolni klavzulo o sestavi kategorij.

Morda boste opazili, da so funkcije v tej kategoriji nadvse preproste. Pravzaprav je skoraj nemogoče imeti napako pri teh funkcijah. Ni nič, nobenih izjem, samo aritmetika in delo s pomnilnikom. Edina slaba stvar, ki se lahko zgodi, je okvara procesorja ali pomnilnika - v tem primeru morate program vseeno zrušiti - vendar se to zgodi zelo redko.

Ali ne bi bilo lepo, če bi vsa naša koda delovala samo na tej ravni stabilnosti? Vsekakor! Kaj pa na primer V / I? Brez tega vsekakor ne moremo živeti. Tukaj rešijo rešitve monad: izolirajo vse nestabilne operacije v zelo majhne in zelo dobro revidirane koščke - potem lahko v celotni aplikaciji uporabite stabilne izračune!

Vnesite Monade

Recimo nestabilno vedenje, kot je I / O a stranski učinek . Zdaj želimo imeti možnost delati z vsemi našimi prej definiranimi funkcijami, kot je length in tipi, kot so String ob prisotnosti tega na stabilen način stranski učinek .

Začnimo torej s prazno kategorijo $ M [A] $ in jo spremenimo v kategorijo, ki bo imela vrednosti z določeno vrsto neželenih učinkov in tudi vrednosti brez stranskih učinkov. Predpostavimo, da smo to kategorijo opredelili in je prazna. Trenutno z njim ne moremo storiti ničesar koristnega, zato bomo upoštevali te tri korake:

  1. Izpolnite z vrednostmi vrst iz kategorije $ A $, na primer String, Int, Double itd. (Zelena polja na spodnjem diagramu)
  2. Ko imamo te vrednosti, z njimi še vedno ne moremo storiti ničesar pomembnega, zato potrebujemo način, kako vsako funkcijo $ f: T prevesti v $ iz $ A $ in ustvariti funkcijo $ g: M [T] v M [U] $ (modre puščice na spodnjem diagramu). Ko imamo te funkcije, lahko z vrednostmi v kategoriji $ M [A] $ naredimo vse, kar smo lahko storili v kategoriji $ A $.
  3. Zdaj, ko imamo povsem novo kategorijo $ M [A] $, se pojavi nov razred funkcij s podpisom $ h: T do M [U] $ (rdeče puščice na spodnjem diagramu). Pojavijo se kot rezultat spodbujanja vrednot v prvem koraku kot del naše kode, tj. Po potrebi jih zapišemo; to so glavne stvari, ki bodo razlikovale delo z $ M [A] $ od dela z $ A $. Zadnji korak bo, da bodo te funkcije dobro delovale tudi na vrstah v $ M [A] $, tj. Zmožnost izpeljati funkcijo $ m: M [T] v M [U] $ iz $ h: T do M [U] $

Ustvarjanje nove kategorije: kategoriji A in M ​​[A] ter rdeča puščica iz A

Začnimo torej z opredelitvijo dveh načinov za promocijo vrednosti vrst $ A $ v vrednosti vrst M $ [A] $: ena funkcija brez stranskih učinkov in ena s stranskimi učinki.

  1. Prva se imenuje $ pure $ in je določena za vsako vrednost stabilne kategorije: $ pure: T to M [T] $. Nastale vrednosti $ M [T] $ ne bodo imele nobenih stranskih učinkov, zato se ta funkcija imenuje $ pure $. Npr. Za I / O monado bo $ pure $ takoj vrnil neko vrednost brez možnosti okvare.
  2. Drugi se imenuje $ constructor $ in za razliko od $ pure $ vrne $ M [T] $ z nekaj stranskimi učinki. Primer takšnega $ konstruktorja $ za asinhročno V / I monado je lahko funkcija, ki iz spleta pridobi nekatere podatke in jih vrne kot String. Vrednost, ki jo vrne $ constructor $, bo v tem primeru imela vrsto $ M [String] $.

Zdaj, ko imamo dva načina za promocijo vrednot v $ M [A] $, je od vas kot programerja, da izberete, katero funkcijo boste uporabili, odvisno od vaših ciljev programa. Oglejmo si primer tukaj: želite pridobiti stran HTML, na primer https://www.toptal.com/javascript/option-maybe-either-future-monads-js, in za to naredite funkcijo $ fetch $. Ker bi lahko med pridobivanjem karkoli šlo narobe - pomislite na omrežne napake itd., Boste kot vrnitev te funkcije uporabili $ M [String] $. Tako bo videti nekako tako kot $ fetch: String to M [String] $ in nekje v telesu funkcije bomo tam uporabili $ constructor $ za $ M $.

Zdaj pa predpostavimo, da naredimo lažno funkcijo za testiranje: $ fetchMock: String to M [String] $. Še vedno ima isti podpis, vendar tokrat samo vbrizgamo nastalo stran HTML v telo $ fetchMock $, ne da bi pri tem izvajali nestabilne omrežne operacije. Torej v tem primeru za izvajanje $ fetchMock $ uporabimo samo $ pure $.

Kot naslednji korak potrebujemo funkcijo, ki varno promovira poljubno poljubno funkcijo $ f $ iz kategorije $ A $ do $ M [A] $ (modre puščice v diagramu). Ta funkcija se imenuje $ map: (T to U) to (M [T] to M [U]) $.

Zdaj imamo kategorijo (ki ima lahko neželene učinke, če uporabimo $ constructor $), ki ima tudi vse funkcije iz stabilne kategorije, kar pomeni, da so stabilne tudi v $ M [A] $. Morda boste opazili, da smo izrecno uvedli še en razred funkcij, kot je $ f: T v M [U] $. Npr. $ Pure $ in $ constructor $ sta primera takšnih funkcij za $ U = T $, vendar bi jih očitno lahko bilo več, na primer, če bi uporabili $ pure $ in nato $ map $. Torej na splošno potrebujemo način za obravnavo poljubnih funkcij v obliki $ f: T do M [U] $.

Če želimo na podlagi $ f $ izdelati novo funkcijo, ki bi jo lahko uporabili na $ M [T] $, lahko poskusimo uporabiti $ map $. Toda to nas bo pripeljalo do funkcije $ g: M [T] do M [M [U]] $, kar ni dobro, saj ne želimo imeti še ene kategorije $ M [M [A]] $. Za reševanje te težave uvedemo še zadnjo funkcijo: $ flatMap: (T do M [U]) do (M [T] do M [U]) $.

Toda zakaj bi to želeli storiti? Predpostavimo, da sledimo koraku 2, torej imamo $ pure $, $ constructor $ in $ map $. Recimo, da želimo zajeti stran HTML s spletnega mesta toptal.com, nato optično prebrati vse URL-je tam in jih pridobiti. Naredil bi funkcijo $ fetch: String to M [String] $, ki pridobi samo en URL in vrne stran HTML.

Nato bi to funkcijo uporabil za URL in poiskal stran s strani toptal.com, kar je $ x: M [String] $. Zdaj naredim nekaj transformacije na $ x $ in končno pridem do nekega URL-ja $ u: M [String] $. Zanjo želim uporabiti funkcijo $ fetch $, vendar je ne morem, ker ima vrsto $ String $, ne $ M [String] $. Zato potrebujemo $ flatMap $ za pretvorbo $ fetch: String v M [String] $ v $ m_fetch: M [String] v M [String] $.

Zdaj, ko smo zaključili vse tri korake, lahko dejansko sestavimo poljubne vrednostne transformacije, ki jih potrebujemo. Če imate na primer vrednost $ x $ tipa $ M [T] $ in $ f: T to U $, lahko uporabite $ map $, da $ f $ uporabite za vrednost $ x $ in dobite vrednost $ y $ vrste $ M [U] $. Tako je mogoče kakršno koli preoblikovanje vrednosti izvesti na 100 odstotkov brez napak, če so izvedbe $ pure $, $ constructor $, $ map $ in $ flatMap $ brez napak.

Namesto da bi se vsakič, ko jih srečate v svoji zbirki kod, spoprijeli z nekaterimi neprijetnimi učinki, se morate prepričati, da so samo te štiri funkcije pravilno izvedene. Na koncu programa boste dobili samo en $ M [X] $, kjer lahko varno odvijete vrednost $ X $ in obravnavate vse primere napak.

To je tisto, kar je monada: stvar, ki izvaja $ pure $, $ map $ in $ flatMap $. (Pravzaprav $ map $ lahko dobimo iz $ pure $ in $ flatMap $, vendar je zelo uporabna in razširjena funkcija, zato je nisem izpustil iz definicije.)

Možnost Monada, imenovana Monada Monada

V redu, poglobimo se v praktično izvajanje in uporabo monad. Prva resnično koristna monada je monada Option. Če prihajate iz klasičnih programskih jezikov, ste verjetno naleteli na številne zrušitve zaradi zloglasne napake ničelnega kazalca. Tony Hoare, izumitelj nič, imenuje ta izum 'Napaka v milijardah dolarjev':

To je povzročilo nešteto napak, ranljivosti in sistemskih zrušitev, ki so v zadnjih štiridesetih letih verjetno povzročile milijardo dolarjev bolečin in škode.

Poskusimo torej to izboljšati. Monada Option bodisi vsebuje neko ničelno vrednost ali pa je nima. Precej podobno ničelni vrednosti, vendar lahko s to monado varno uporabljamo naše natančno določene funkcije, ne da bi se bali izjeme ničelnega kazalca. Oglejmo si izvedbe v različnih jezikih:

JavaScript - možnost Monad / Monad 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 - možnost Monad / Morda Monada

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 - Možnost Monada / Morda Monada

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