)) } } }

Scala - variant Monad / võib-olla 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]

Alustame rakendades Monad klass, mis on kõigi meie monaadi rakenduste alus. Selle klassi omamine on väga mugav, sest rakendades ainult kahte selle meetodit - pure ja flatMap - konkreetse monaadi jaoks saate palju meetodeid tasuta (piirdume nende näites vaid meetodiga map, kuid üldiselt on palju muid kasulikke meetodeid, näiteks sequence ja traverse töötamiseks Monad s) massiividega.

Saame väljendada map koostisena pure ja flatMap. flatMap Allkirjast $ flatMap: (T to M [U]) to (M [T] to M [U]) $ näete, et see on tõesti $ mapi lähedal: (T kuni U) to (M [T] to M [U]) $. Erinevus on täiendav $ M $ keskel, kuid saame kasutada pure funktsioon teisendada $ U $ väärtuseks $ M [U] $. Nii väljendame map flatMap ja pure.

See töötab Scala puhul hästi, kuna sellel on täiustatud tüüpi süsteem. See töötab hästi ka JS-i, Pythoni ja Ruby puhul, kuna need on dünaamiliselt kirjutatud. Kahjuks ei tööta see Swifti puhul, kuna see on staatiliselt sisestatud ja sellel ei ole täiustatud tüüpi funktsioone kõrgemat sorti tüübid , nii et Swifti jaoks peame rakendama map igale monaadile.

Pange tähele ka seda, et Optioni monaad on juba a tegelikult standard selliste keelte jaoks nagu Swift ja Scala, seega kasutame oma monaadi rakendustes veidi erinevaid nimesid.

Nüüd, kui meil on baas Monad klassi, jõuame meie Option monadi rakenduste juurde. Nagu varem mainitud, on põhiidee, et Option kas omab mingit väärtust (nn Some) või ei oma üldse mingit väärtust (None).

pure meetod lihtsalt edendab väärtust väärtusele Some, samas kui flatMap meetod kontrollib Option praegust väärtust - kui see on None siis naaseb None ja kui see on Some alusväärtusega eraldab see alusväärtuse, rakendab f() sellele ja tagastab tulemuse.

Pange tähele, et lihtsalt nende kahe funktsiooni ja map abil on nullkursori erandisse pääsemine võimatu. (Probleem võiks võivad tekkida flatMap rakendamisel meetod, kuid see on vaid paar rida meie koodis, mida kontrollime üks kord. Pärast seda kasutame kogu oma koodi tuhandetes kohtades lihtsalt oma Option monadi rakendust ega pea üldse nullkursori erandit kartma.)

Kumbki monaad

Sukeldume teise monaadi: kas või. See on põhimõtteliselt sama mis Option-monaad, kuid Some -ga nimetatakse Right ja None nimetatakse Left. Kuid seekord Left on lubatud ka selle aluseks olev väärtus.

Me vajame seda, sest erandi viskamine on väga mugav. Kui ilmnes erand, siis Either väärtus saab olema Left(Exception). flatMap funktsioon ei edene, kui väärtus on Left, mis kordab erandite viskamise semantikat: kui erand juhtus, peatame edasise täitmise.

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

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

Kiire - kas 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(

Valik / Võib-olla, Mõlemad ja tulevased monaadid JavaScripti, Pythoni, Rubiini, Swifti ja Scala keeles

See monaadiõpetus annab lühikese selgituse monaadidest ja näitab, kuidas kõige kasulikumaid viies erinevas programmeerimiskeeles rakendada - kui otsite JavaScripti , monaadid sisse Python , monaadid sisse Rubiin , monaadid sisse Kiire ja / või monaadid Redel või rakenduste võrdlemiseks loete õiget artiklit!

Nende monaadide abil saate lahti mitmetest vigadest, nagu null-pointeri erandid, töötlemata erandid ja võistlustingimused.



Seda ma käsitlen allpool:

  • Sissejuhatus kategooriateooriasse
  • Monaadi määratlus
  • Optioni („Võib-olla”) monaadi, kas monaadi ja tulevase monaadi rakendused ning nende abil rakendatav näidisprogramm JavaScripti, Pythoni, Ruby, Swifti ja Scala keeles

Alustame! Meie esimene peatus on kategooriateooria, mis on monaadide aluseks.

Sissejuhatus kategooriateooriasse

Kategooriateooria on matemaatiline valdkond, mida arendati aktiivselt 20. sajandi keskel. Nüüd on see paljude funktsionaalsete programmeerimiskontseptsioonide alus, sealhulgas monaad. Vaatame kiiresti tarkvaraarenduse terminoloogiale kohandatud kategooriateooria kontseptsioone.



Seega on kolm põhimõistet, mis määratlevad a kategooria:

  1. Tüüp on just selline, nagu me seda staatiliselt sisestatud keeltes näeme. Näited: Int, String, Dog, Cat jne.
  2. Funktsioonid ühendage kahte tüüpi. Seetõttu saab neid kujutada nooltena ühelt tüübilt teisele või iseendale. Funktsiooni $ f $ tüübist $ T $ tüübiks $ U $ saab tähistada kui $ f: T kuni U $. Võite mõelda sellest kui programmeerimiskeele funktsioonist, mis võtab argumendi tüübiga $ T $ ja tagastab tüübi $ U $ väärtuse.
  3. Kompositsioon on operatsioon, mida tähistab operaator $ cdot $, mis ehitab olemasolevatest uutest funktsioonidest. Kategoorias on see alati tagatud mis tahes funktsioonide $ f: T to U $ ja $ g: U to V $ jaoks, olemas ainulaadne funktsioon $ h: T to V $. Seda funktsiooni tähistatakse kui $ f cdot g $. Operatsioon kaardistab funktsioonide paari tõhusalt teise funktsiooniga. Programmeerimiskeeltes on see toiming muidugi alati võimalik. Näiteks kui teil on funktsioon, mis tagastab stringi pikkuse - $ strlen: String to Int $ - ja funktsioon, mis ütleb, kas arv on paariline - $ even: Int Boolean $ - siis saate teha function $ even { _} strlen: String Boolean $ -ni, mis ütleb, kas String on ühtlane. Sel juhul on $ even { _} strlen = even cdot strlen $. Koosseis eeldab kahte omadust:
    1. Assotsiatiivsus: $ f cdot g cdot h = (f cdot g) cdot h = f cdot (g cdot h) $
    2. Identiteedifunktsiooni olemasolu: $ forall T: eksisteerib f: T to T $ või lihtsas inglise keeles, iga tüübi $ T $ jaoks on olemas funktsioon, mis kaardistab $ T $ endale.

Nii et vaatame lihtsat kategooriat.

Lihtne kategooria, mis hõlmab stringi, int ja topelt ning mõned funktsioonid nende hulgas.



Ääremärkus: eeldame, et Int, String ja kõik muud siin olevad tüübid on garanteeritud mitte-nullid, st null-väärtust ei eksisteeri.

Lisamärkus 2: see on tegelikult ainult osa kategooriasse, kuid see on kõik, mida meie aruteluks soovime, sest sellel on kõik vajalikud osad ja skeem on sel viisil vähem segamini. Reaalses kategoorias oleks ka kõik koosnevad funktsioonid, näiteks $ roundToString: Double to String = intToString cdot round $, et kategooriate koosseisuklausel rahuldada.

Võite märgata, et selle kategooria funktsioonid on ülilihtsad. Tegelikult on nendes funktsioonides viga peaaegu võimatu. Puuduvad nullid, pole erandeid, on ainult aritmeetika ja töötamine mäluga. Nii et ainus halb asi, mis võib juhtuda, on protsessori või mälu rike - sellisel juhul peate ikkagi programmi krahhi tegema -, kuid see juhtub väga harva.

Kas poleks tore, kui kogu meie kood toimiks just sellel stabiilsuse tasemel? Absoluutselt! Aga kuidas on lood näiteks I / O-ga? Ilma selleta ei saa me kindlasti elada. Siin tulevad appi monaadilahendused: need eraldavad kõik ebastabiilsed toimingud üliväikesteks ja väga hästi kontrollitud koodijuppideks - siis saate stabiilseid arvutusi kasutada kogu oma rakenduses!

Sisestage Monad

Nimetagem ebastabiilset käitumist nagu I / O a kõrvalmõju . Nüüd tahame, et saaksime töötada kõigi oma eelnevalt määratletud funktsioonidega, näiteks length ja tüübid nagu String selle olemasolul stabiilselt kõrvalmõju .

Alustagem siis tühjast kategooriast $ M [A] $ ja tehke see kategooriasse, millel on ühe kindla kõrvalmõjuga väärtused ja ka kõrvalnähtudeta väärtused. Oletame, et oleme selle kategooria määratlenud ja see on tühi. Praegu pole sellega midagi kasulikku teha, nii et selle kasulikuks muutmiseks järgime neid kolme sammu:

  1. Täitke see kategooria $ A $ tüüpide väärtustega, näiteks String, Int, Double jne (rohelised kastid alloleval diagrammil)
  2. Kui need väärtused on olemas, ei saa me ikkagi nendega midagi sisukat teha, seega vajame viisi, kuidas võtta iga funktsioon $ f: T kuni U $ alates $ A $ ja luua funktsioon $ g: M [T] kuni M [U] $ (sinised nooled alloleval diagrammil). Kui need funktsioonid on meil olemas, saame kategooria $ M [A] $ väärtustega teha kõike, mida suutsime teha kategoorias $ A $.
  3. Nüüd, kui meil on täiesti uus kategooria $ M [A] $, tekib uus funktsioonide klass allkirjaga $ h: T kuni M [U] $ (punased nooled alloleval diagrammil). Need tekivad väärtuste propageerimise tulemusena esimeses etapis osana meie koodibaasist, st me kirjutame need vastavalt vajadusele; need on peamised asjad, mis eristavad töötamist teenusega $ M [A] $ versus töötamist funktsiooniga $ A $. Viimane samm on panna need funktsioonid ka $ M [A] $ tüüpides hästi tööle, st tuletada funktsioon $ m: M [T] kuni M [U] $ väärtusest $ h: T väärtuseni M [U] $

Uue kategooria loomine: A- ja M-kategooria [A], lisaks punane nool A-st

Alustuseks määratleme kaks viisi, kuidas reklaamida tüüpide $ A $ väärtusi tüüpide $ M [A] $ väärtusteks: üks funktsioon ilma kõrvalmõjudeta ja teine ​​kõrvalmõjudeta.

  1. Esimest nimetatakse $ pure $ ja see on määratletud iga stabiilse kategooria väärtuse jaoks: $ pure: T kuni M [T] $. Saadud $ M [T] $ väärtustel pole kõrvaltoimeid, seetõttu nimetatakse seda funktsiooni $ pure $. Näiteks sisend- / väljundmonaadi korral tagastab $ pure $ mõne väärtuse kohe ilma ebaõnnestumisvõimaluseta.
  2. Teine on $ constructor $ ja erinevalt $ pure $ tagastab $ M [T] $ koos mõnede kõrvalmõjudega. Asünkroonse sisend- / väljundmonaadi sellise $ konstruktori $ näiteks võib olla funktsioon, mis tõmbab osa andmeid veebist ja tagastab need kui String. $ Constructor $ tagastatud väärtuse tüüp on sel juhul $ M [String] $.

Nüüd, kui meil on kaks võimalust väärtuste reklaamimiseks dollarites M [A] $, on teie kui programmeerija valik, millist funktsiooni kasutada, sõltuvalt teie programmi eesmärkidest. Vaatleme siin ühte näidet: soovite hankida HTML-i lehe nagu https://www.toptal.com/javascript/option-maybe-either-future-monads-js ja selleks teete funktsiooni $ fetch $. Kuna selle toomisel võib midagi valesti minna - mõelge võrgutõrgetest jne -, kasutate selle funktsiooni tagastustüübina $ M [String] $. Nii näeb see välja umbes selline nagu $ fetch: String to M [String] $ ja kusagil seal funktsiooni kehas kasutame $ constructor $ for $ M $.

Oletame, et teeme testimiseks proovifunktsiooni: $ fetchMock: String to M [String] $. Sellel on endiselt sama allkiri, kuid seekord süstime lihtsalt saadud HTML-i lehe $ fetchMock $ sisemusse, tegemata ebastabiilseid võrgutoiminguid. Nii et sel juhul kasutame rakenduse $ fetchMock $ rakendamiseks lihtsalt dollarit $ pure $.

Järgmise sammuna vajame funktsiooni, mis reklaamib ohutult suvalist funktsiooni $ f $ kategooriast $ A $ kuni $ M [A] $ (sinised nooled diagrammil). Seda funktsiooni nimetatakse $ map: (T kuni U) to (M [T] to M [U]) $.

Nüüd on meil kategooria (millel võivad olla kõrvalmõjud, kui kasutame $ constructor $), millel on ka kõik funktsioonid stabiilsest kategooriast, mis tähendab, et need on stabiilsed ka $ M [A] $ väärtuses. Võib-olla märkate, et võtsime selgesõnaliselt kasutusele veel ühe klassi funktsioone, näiteks $ f: T to M [U] $. Näiteks $ pure $ ja $ constructor $ on selliste funktsioonide näited $ U = T $ jaoks, kuid neid võib ilmselt olla rohkem, näiteks kui kasutaksime $ pure $ ja seejärel $ map $. Niisiis vajame üldiselt võimalust meelevaldsete funktsioonide lahendamiseks kujul $ f: T kuni M [U] $.

Kui soovime teha uue funktsiooni $ f $ põhjal, mida saaks rakendada rakendusele $ M [T] $, võiksime proovida kasutada funktsiooni $ map $. Kuid see paneb meid toimima $ g: M [T] kuni M [M [U]] $, mis pole hea, kuna me ei soovi omada veel ühte kategooriat $ M [M [A]] $. Selle probleemi lahendamiseks tutvustame veel ühte funktsiooni: $ flatMap: (T to M [U]) to (M [T] to M [U]) $.

Aga miks me tahaksime seda teha? Oletame, et oleme 2. sammu järel, st meil on $ pure $, $ constructor $ ja $ map $. Oletame, et tahame haarata saidilt toptal.com HTML-lehe, seejärel skannida kõik seal olevad URL-id ja neid ka tuua. Teeksin funktsiooni $ fetch: String to M [String] $, mis tõmbab ainult ühe URL-i ja tagastab HTML-lehe.

Seejärel rakendaksin selle funktsiooni URL-ile ja saaksin lehe saidilt toptal.com, milleks on $ x: M [String] $. Nüüd teisendan teenust $ x $ ja jõuan lõpuks URL-ile $ u: M [String] $. Ma tahan sellele rakendada funktsiooni $ fetch $, kuid ma ei saa seda teha, sest selleks on vajalik tüüp $ String $, mitte $ M [String] $. Sellepärast vajame teenuse $ fetch: String to M [String] $ teisendamiseks $ m_fetch: $ flatMap $: M [String] to M [String] $.

Nüüd, kui oleme kõik kolm sammu läbinud, saame tegelikult koostada kõik vajalikud väärtuste teisendused. Näiteks kui teil on väärtus $ x $ tüüpi $ M [T] $ ja $ f: T kuni U $, saate $ map $ abil rakendada $ f $ väärtusele $ x $ ja saada väärtust $ y $ tüübist $ M [U] $. Nii saab väärtuste mis tahes muundamise teha 100-protsendiliselt veatult, kui rakendused $ pure $, $ constructor $, $ map $ ja $ flatMap $ on veatud.

Nii et selle asemel, et iga kord, kui neid oma koodibaasis kohtate, peaksite tegelema mõne vastiku efektiga, peate lihtsalt veenduma, et ainult neid nelja funktsiooni rakendatakse õigesti. Programmi lõpus saate ainult ühe $ M [X] $, kus saate väärtuse $ X $ turvaliselt lahti pakkida ja kõigi veajuhtumitega hakkama saada.

See on monaad: asi, mis rakendab dollareid $ pure $, $ map $ ja $ flatMap $. (Tegelikult saab $ map $ tuletada lausetest $ pure $ ja $ flatMap $, kuid see on väga kasulik ja laialt levinud funktsioon, nii et ma ei jätnud seda definitsioonist välja.)

Valik Monad, teise nimega Võib-olla Monad

Olgu, sukeldume monaadide praktilisse rakendamisse ja kasutusse. Esimene tõeliselt abivalmis monaad on Optioni monaad. Kui kasutate klassikalisi programmeerimiskeeli, olete tõenäoliselt kokku puutunud kurikuulsa nullkursori vea tõttu paljude krahhidega. Nulli leiutaja Tony Hoare nimetab seda leiutist 'Miljardi dollari veaks':

See on toonud kaasa lugematuid vigu, haavatavusi ja süsteemi krahhe, mis on viimase neljakümne aasta jooksul põhjustanud tõenäoliselt miljard dollarit valu ja kahju.

Nii et proovime seda parandada. Optioni monaadil on kas mõni nullväärtus või see puudub. Päris sarnane nullväärtusega, kuid omades seda monaadi, saame turvaliselt kasutada oma täpselt määratletud funktsioone, kartmata nullkursori erandit. Vaatame rakendusi erinevates keeltes:

JavaScript - valik Monad / Võib-olla 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 - võimalus Monad / Võib-olla 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()

Rubiin - võimalus Monad / Võib-olla 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 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