Monadi JavaScript rese semplici

Smoke Art Cubes to Smoke - MattysFlicks - (CC BY 2.0)
Nota: fa parte della serie "Composing Software" (ora un libro!) Sull'apprendimento da zero della programmazione funzionale e delle tecniche software compositive in JavaScript ES6 +. Rimanete sintonizzati. C'è molto di più a venire!

Prima di iniziare a imparare le monadi, dovresti già sapere:

  • Composizione della funzione: componi (f, g) (x) = (f ∘ g) (x) = f (g (x))
  • Nozioni di base di Functor: comprensione dell'operazione Array.map ().
"Una volta che capisci le monadi, diventi immediatamente incapace di spiegarle a chiunque altro" La maledizione di Lady Monadgreen ~ Gilad Bracha (usata notoriamente da Douglas Crockford)
“Dr. Hoenikker diceva che qualsiasi scienziato che non era in grado di spiegare a un bambino di otto anni quello che stava facendo era un ciarlatano. "~ Il romanzo di Cat Cradle di Kurt Vonnegut

Se vai alla ricerca di "monade" su Internet, verrai bombardato da impenetrabile teoria della categoria matematica e un gruppo di persone che spiegano "utile" monadi in termini di burritos e tute spaziali.

Le monadi sono semplici. Il gergo è difficile. Tagliamo all'essenza.

Una monade è un modo di comporre funzioni che richiedono contesto oltre al valore di ritorno, come calcolo, ramificazione o I / O. Le monadi digitano lift, appiattire e mappare in modo che i tipi si allineino per le funzioni di sollevamento a => M (b), rendendole componibili. È una mappatura da un tipo a a un tipo b insieme a un contesto computazionale, nascosto nei dettagli di implementazione di lift, flat e map:

  • Mappa delle funzioni: a => b
  • Mappa di Functor con contesto: Functor (a) => Functor (b)
  • Monadi appiattiti e mappa con contesto: Monade (Monade (a)) => Monade (b)

Ma cosa significano "appiattire", "mappa" e "contesto"?

  • Mappa significa "applica una funzione a a e restituisce a b". Dato un input, restituisci un output.
  • Il contesto è il dettaglio computazionale della composizione della monade (incluso ascensore, appiattimento e mappa). L'API Functor / Monad e il suo funzionamento forniscono il contesto che consente di comporre la monade con il resto dell'applicazione. Il punto di funzione e monade è quello di sottrarre quel contesto in modo che non ci dobbiamo preoccupare mentre stiamo componendo le cose. La mappatura all'interno del contesto significa che si applica una funzione da a => b al valore all'interno del contesto e si restituisce un nuovo valore b racchiuso nello stesso tipo di contesto. Osservabili a sinistra? Osservabili a destra: osservabile (a) => osservabile (b). Matrici sul lato sinistro? Matrici sul lato destro: Array (a) => Array (b).
  • Tipo lift significa elevare un tipo in un contesto, benedire il valore con un'API che puoi usare per calcolare da quel valore, innescare calcoli contestuali, ecc ... a => F (a) (Le monadi sono una specie di funzione).
  • Appiattire significa scartare il valore dal contesto. F (a) => a.

Esempio:

const x = 20; // Alcuni dati di tipo `a`
const f = n => n * 2; // Una funzione da `a` a` b`
const arr = Array.of (x); // Il tipo lift.
// JS ha zucchero di tipo lift per array: [x]
// .map () applica la funzione f al valore x
// nel contesto dell'array.
const risultato = arr.map (f); // [40]

In questo caso, Array è il contesto e x è il valore su cui stiamo mappando.

Questo esempio non includeva matrici di matrici, ma è possibile appiattire le matrici in JS con .concat ():

[] .concat.apply ([], [[1], [2, 3], [4]]); // [1, 2, 3, 4]

Probabilmente stai già usando le monadi.

Indipendentemente dal tuo livello di abilità o comprensione della teoria delle categorie, l'uso delle monadi semplifica la gestione del codice. Non riuscire a sfruttare le monadi può rendere più difficile il funzionamento del codice (ad es. Inferno di richiamata, rami condizionati nidificati, maggiore verbosità).

Ricorda, l'essenza dello sviluppo del software è la composizione e le monadi rendono la composizione più semplice. Dai un'occhiata all'essenza di cosa sono le monadi:

  • Mappa delle funzioni: a => b che consente di comporre funzioni di tipo a => b
  • Mappa di Functor con contesto: Functor (a) => Functor (b), che consente di comporre le funzioni F (a) => F (b)
  • Le monadi appiattiscono e mappano con il contesto: Monade (Monade (a)) => Monade (b), che consente di comporre le funzioni di sollevamento a => F (b)

Questi sono solo modi diversi di esprimere la composizione delle funzioni. L'intera ragione per cui le funzioni esistono è che puoi comporle. Le funzioni ti aiutano a scomporre i problemi complessi in problemi semplici che sono più facili da risolvere in modo isolato, in modo da poterli comporre in vari modi per formare la tua applicazione.

La chiave per comprendere le funzioni e il loro corretto utilizzo è una comprensione più profonda della composizione delle funzioni.

La composizione della funzione crea pipeline di funzioni attraverso cui i tuoi dati scorrono. Inserite alcuni input nella prima fase della pipeline e alcuni dati escono dall'ultima fase della pipeline, trasformati. Ma affinché funzioni, ogni fase della pipeline deve aspettarsi il tipo di dati restituito dalla fase precedente.

Comporre semplici funzioni è facile, perché i tipi si allineano facilmente. Basta abbinare il tipo di output b al tipo di input b e sei in affari:

g: a => b
f: b => c
h = f (g (a)): a => c

La composizione con i funzioni è anche facile se stai mappando F (a) => F (b) perché i tipi si allineano:

g: F (a) => F (b)
f: F (b) => F (c)
h = f (g (Fa)): F (a) => F (c)

Ma se vuoi comporre le funzioni da a => F (b), b => F (c) e così via, hai bisogno di monadi. Sostituiamo la F () con M () per chiarire questo:

g: a => M (b)
f: b => M (c)
h = composeM (f, g): a => M (c)

Ops. In questo esempio, i tipi di funzione del componente non si allineano! Per l'input di f, volevamo il tipo b, ma quello che ottenevamo era il tipo M (b) (una monade di b). A causa di tale disallineamento, composeM () deve scartare M (b) che g restituisce in modo da poterlo passare a f, perché f si aspetta il tipo b, non il tipo M (b). Quel processo (spesso chiamato .bind () o .chain ()) è dove si verificano l'appiattimento e la mappa.

Svolge la b da M (b) prima di passarla alla funzione successiva, che porta a questo:

g: a => M (b) si appiattisce a => b
f: b mappa a => M (c)
h composeM (f, g):
               a appiattire (M (b)) => b => mappa (b => M (c)) => M (c)

Le monadi allineano i tipi per le funzioni di sollevamento a => M (b), in modo da poterle comporre.

Nel diagramma sopra, l'appiattimento da M (b) => b e la mappa da b => M (c) avviene all'interno della catena da a => M (c). La chiamata a catena viene gestita all'interno di composeM (). Ad alto livello, non devi preoccuparti. Puoi semplicemente comporre le funzioni di ritorno della monade usando lo stesso tipo di API che utilizzeresti per comporre le normali funzioni.

Le monadi sono necessarie perché molte funzioni non sono semplici mappature da a => b. Alcune funzioni devono affrontare gli effetti collaterali (promesse, flussi), gestire le ramificazioni (forse), gestire le eccezioni (entrambi), ecc ...

Ecco un esempio più concreto. Che cosa succede se è necessario recuperare un utente da un'API asincrona e quindi passare i dati dell'utente a un'altra API asincrona per eseguire alcuni calcoli ?:

getUserById (id: String) => Promessa (Utente)
hasPermision (Utente) => Promessa (booleana)

Scriviamo alcune funzioni per dimostrare il problema. Innanzitutto, le utility, compose () e trace ():

const compose = (... fns) => x => fns.reduceRight ((y, f) => f (y), x);
const trace = label => value => {
  console.log (`$ {label}: $ {value}`);
  valore di ritorno;
};

Quindi alcune funzioni per comporre:

{
  const label = 'Composizione chiamata API';
  // a => Promessa (b)
  const getUserById = id => id === 3?
    Promise.resolve ({nome: 'Kurt', ruolo: 'Autore'}):
    non definito
  ;
  // b => Promessa (c)
  const hasPermission = ({role}) => (
    Promise.resolve (ruolo === "Autore")
  );
  // Prova a comporli. Attenzione: questo fallirà.
  const authUser = compose (hasPermission, getUserById);
  // Oops! Sempre falso!
  authuser (3) .then (trace (etichetta));
}

Quando proviamo a comporre hasPermission () con getUserById () per formare authUser () ci imbattiamo in un grosso problema perché hasPermission () si aspetta un oggetto Utente e ottiene invece una Promessa (Utente). Per risolvere questo problema, dobbiamo sostituire compose () per composePromises () - una versione speciale di compose che sa che è necessario utilizzare .then () per realizzare la composizione della funzione:

{
  const composeM = chainMethod => (... ms) => (
    ms.reduce ((f, g) => x => g (x) [chainMethod] (f))
  );
  const composePromises = composeM ('then');
  const label = 'Composizione chiamata API';
  // a => Promessa (b)
  const getUserById = id => id === 3?
    Promise.resolve ({nome: 'Kurt', ruolo: 'Autore'}):
    non definito
  ;
  // b => Promessa (c)
  const hasPermission = ({role}) => (
    Promise.resolve (ruolo === "Autore")
  );
  // Componi le funzioni (funziona!)
  const authUser = composePromises (hasPermission, getUserById);
  authuser (3) .then (trace (etichetta)); // vero
}

Entreremo in quello che sta facendo composeM (), più tardi.

Ricorda l'essenza delle monadi:

  • Mappa delle funzioni: a => b
  • Mappa di Functor con contesto: Functor (a) => Functor (b)
  • Monadi appiattiti e mappa con contesto: Monade (Monade (a)) => Monade (b)

In questo caso, le nostre monadi sono davvero promesse, quindi quando componiamo queste funzioni di ritorno delle promesse, abbiamo una Promessa (Utente) anziché l'Utente che hasPermission () si aspetta. Nota che se togliessi il wrapper Monad () esterno da Monad (Monad (a)), rimarrai con Monad (a) => Monad (b), che è solo il normale funzione .map (). Se avessimo qualcosa che potesse appiattire Monade (x) => x, saremmo in affari.

Di cosa sono fatte le Monadi

Una monade si basa su una semplice simmetria - Un modo per avvolgere un valore in un contesto e un modo per scartare il valore dal contesto:

  • Lift / Unit: un tipo di ascensore da un certo tipo nel contesto della monade: a => M (a)
  • Appiattisci / Unisci: non sposta il tipo dal contesto: M (a) => a

E poiché le monadi sono anche funzioni, possono anche mappare:

  • Mappa: Mappa con contesto conservato: M (a) -> M (b)

Combina l'appiattimento con la mappa e otterrai la composizione della funzione catena per funzioni di sollevamento della monade, nota anche come composizione Kleisli, che prende il nome da Heinrich Kleisli:

  • FlatMap / Chain: Flatten + map: M (M (a)) => M (b)

Per le monadi, i metodi .map () sono spesso omessi dall'API pubblica. Lift + flatten non spiega esplicitamente .map (), ma hai tutti gli ingredienti necessari per farlo. Se puoi sollevare (aka di / unit) e chain (aka bind / flatMap), puoi creare .map ():

const MyMonad = value => ({
  // <... inserisce una catena arbitraria e di qui ...>
  mappa (f) {
    restituisce this.chain (a => this.constructor.of (f (a)));
  }
});

Quindi, se definisci .of () e .chain () /. Join () per la tua monade, puoi dedurre la definizione di .map ().

L'ascensore è il metodo factory / constructor e / o constructor.of (). Nella teoria delle categorie, si chiama "unità". Tutto ciò che fa è sollevare il tipo nel contesto della monade. Trasforma una a in una monade di una.

In Haskell, è (molto confusamente) chiamato return, che diventa estremamente confuso quando provi a parlarne ad alta voce perché quasi tutti lo confondono con i rendimenti delle funzioni. Quasi sempre lo chiamo "lift" o "type lift" in prosa e .of () nel codice.

Quel processo di appiattimento (senza la mappa in .chain ()) è di solito chiamato flatten () o join (). Spesso (ma non sempre), flatten () / join () viene completamente omesso perché incorporato in .chain () /. FlatMap (). L'appiattimento è spesso associato alla composizione, quindi è spesso combinato con la mappatura. Ricorda che per comporre a => M (a) sono necessarie entrambe le funzioni + wrapping + map.

A seconda del tipo di monade con cui hai a che fare, il processo di scartamento potrebbe essere estremamente semplice. Nel caso della monade identità, è proprio come .map (), tranne per il fatto che non ripristini il valore risultante nel contesto della monade. Ciò ha l'effetto di scartare uno strato di avvolgimento:

{// Monade identitaria
const Id = value => ({
  // Mappatura di Functor
  // Conserva il wrapping per .map () di
  // passa il valore mappato nel tipo
  // sollevamento:
  mappa: f => Id.of (f (valore)),
  // Concatenamento della monade
  // Elimina un livello di avvolgimento
  // omettendo l'ascensore di tipo .of ():
  catena: f => f (valore),
  // Solo un modo conveniente per ispezionare
  // i valori:
  toString: () => `Id ($ {value})`
});
// L'ascensore di tipo per questa monade è giusto
// un riferimento alla fabbrica.
Id.of = Id;

Ma la parte da scartare è anche dove si nascondono in genere le cose strane come gli effetti collaterali, la diramazione degli errori o l'attesa dell'I / O asincrono. In tutto lo sviluppo del software, la composizione è dove accadono tutte le cose veramente interessanti.

Ad esempio, con le promesse, .chain () chiamato .then (). Chiamare promise.then (f) non invocherà subito f (). Invece, attenderà che la promessa si risolva, quindi chiamerà f () (da cui il nome).

Esempio:

{
  const x = 20; // Il valore
  const p = Promise.resolve (x); // Il contesto
  const f = n =>
    Promise.resolve (n * 2); // La funzione
  const const = p.then (f); // L'applicazione
  result.then (
    r => console.log (r) // 40
  );
}

Con le promesse, viene utilizzato .then () invece di .chain (), ma è quasi la stessa cosa.

Potresti aver sentito che una promessa non è strettamente una monade. Questo perché scarterà la promessa esterna solo se il valore è una promessa per cominciare. Altrimenti, .then () si comporta come .map ().

Ma poiché si comporta diversamente per i valori di promessa e altri valori, .then () non obbedisce rigorosamente a tutte le leggi matematiche che tutti i funzioni e / o le monadi devono soddisfare per tutti i valori dati. In pratica, fintanto che sei consapevole di tale ramificazione comportamentale, di solito puoi trattarli come entrambi. Basta essere consapevoli del fatto che alcuni strumenti di composizione generici potrebbero non funzionare come previsto con le promesse.

Costruire composizione monadica (aka Kleisli)

Diamo uno sguardo più approfondito alla funzione composeM che abbiamo usato per comporre le funzioni promettenti:

const composeM = method => (... ms) => (
  ms.reduce ((f, g) => x => g (x) [metodo] (f))
);

In quello strano riduttore è nascosta la definizione algebrica della composizione della funzione: f (g (x)). Rendiamo più facile individuare:

{
  // La definizione algebrica della composizione delle funzioni:
  // (f ∘ g) (x) = f (g (x))
  const compose = (f, g) => x => f (g (x));
  const x = 20; // Il valore
  const arr = [x]; // Il container
  // Alcune funzioni da comporre
  const g = n => n + 1;
  const f = n => n * 2;
  // Prova che .map () realizza la composizione della funzione.
  // Concatenare le chiamate alla mappa è composizione delle funzioni.
  trace ('map composes') ([
    arr.map (g) .map (f),
    arr.map (compose (f, g))
  ]);
  // => [42], [42]
}

Ciò significa che potremmo scrivere un'utilità di composizione generalizzata che dovrebbe funzionare per tutti i funzioni che forniscono un metodo .map () (ad es. Array):

const composeMap = (... ms) => (
  ms.reduce ((f, g) => x => g (x) .map (f))
);

Questa è solo una leggera riformulazione dello standard f (g (x)). Dato un numero qualsiasi di funzioni di tipo a -> Functor (b), scorrere ogni funzione e applicare ognuna al suo valore di input, x. Il metodo .reduce () accetta una funzione con due valori di input: un accumulatore (f in questo caso) e l'elemento corrente nell'array (g).

Restituiamo una nuova funzione x => g (x) .map (f) che diventa f nella prossima applicazione. Abbiamo già dimostrato sopra che x => g (x) .map (f) equivale a sollevare compose (f, g) (x) nel contesto del funzione. In altre parole, equivale ad applicare f (g (x)) ai valori nel contenitore: in questo caso, si applicherebbe la composizione ai valori all'interno dell'array.

Avviso sulle prestazioni: non lo sto raccomandando per gli array. La composizione di funzioni in questo modo richiederebbe più iterazioni sull'intero array (che potrebbe contenere centinaia di migliaia di elementi). Per le mappe su un array, componi prima le semplici funzioni a -> b, quindi esegui il mapping sull'array una volta o ottimizza le iterazioni con .reduce () o un trasduttore.

Per applicazioni con funzione sincera e desiderosa su dati array, questo è eccessivo. Tuttavia, molte cose sono asincrone o pigre e molte funzioni devono gestire cose disordinate come la diramazione per eccezioni o valori vuoti.

È qui che arrivano le monadi. Le monadi possono fare affidamento su valori che dipendono da precedenti azioni asincrone o di ramificazione nella catena di composizione. In questi casi, non è possibile ottenere un valore semplice per composizioni di funzioni semplici. Le tue azioni di ritorno della monade assumono la forma a => Monade (b) anziché a => b.

Ogni volta che hai una funzione che accetta alcuni dati, colpisce un'API e restituisce un valore corrispondente e un'altra funzione che accetta quei dati, colpisce un'altra API e restituisce il risultato di un calcolo su quei dati, ti consigliamo di comporre funzioni di tipo a => Monade (b). Poiché le chiamate API sono asincrone, dovrai racchiudere i valori di ritorno in qualcosa di simile a una promessa o osservabile. In altre parole, le firme per quelle funzioni sono rispettivamente a -> Monade (b) e b -> Monade (c).

Le funzioni di composizione di tipo g: a -> b, f: b -> c sono facili perché i tipi si allineano: h: a -> c è solo a => f (g (a)).

Funzioni di composizione di tipo g: a -> Monade (b), f: b -> Monade (c) è un po 'più difficile: h: a -> Monade (c) non è solo a => f (g (a)) perché f si aspetta b, non Monade (b).

Diventiamo un po 'più concreti e componiamo un paio di funzioni asincrone che restituiscono una promessa:

{
  const label = 'Composizione promessa';
  const g = n => Promise.resolve (n + 1);
  const f = n => Promise.resolve (n * 2);
  const h = composePromises (f, g);
  h (20)
    .poi (trace (etichetta))
  ;
  // Composizione promessa: 42
}

Come scriviamo composePromises () in modo che il risultato sia registrato correttamente? Suggerimento: l'hai già visto.

Ricorda la nostra funzione composeMap ()? Tutto quello che devi fare è cambiare la chiamata .map () in .then (). Promise.then () è fondamentalmente un .map asincrono ().

{
  const composePromises = (... ms) => (
    ms.reduce ((f, g) => x => g (x) .then (f))
  );
  const label = 'Composizione promessa';
  const g = n => Promise.resolve (n + 1);
  const f = n => Promise.resolve (n * 2);
  const h = composePromises (f, g);
  h (20)
    .poi (trace (etichetta))
  ;
  // Composizione promessa: 42
}

La parte strana è che quando si preme la seconda funzione, f (ricordare, f dopo g), il valore di input è una promessa. Non è di tipo b, è di tipo Promise (b), ma f prende il tipo b, non scartato. Quindi cosa sta succedendo?

All'interno di .then (), c'è un processo di scartamento che va da Promessa (b) -> b. Quell'operazione si chiama join o appiattisci.

Potresti aver notato che composeMap () e composePromises () sono funzioni quasi identiche. Questo è il caso d'uso perfetto per una funzione di ordine superiore che può gestire entrambi. Mescoliamo semplicemente il metodo chain in una funzione curry, quindi usiamo la notazione parentesi quadra:

const composeM = method => (... ms) => (
  ms.reduce ((f, g) => x => g (x) [metodo] (f))
);

Ora possiamo scrivere implementazioni specializzate come questa:

const composePromises = composeM ('then');
const composeMap = composeM ('map');
const composeFlatMap = composeM ('flatMap');

Le leggi della monade

Prima di poter iniziare a costruire le tue monadi, devi sapere che ci sono tre leggi che tutte le monadi dovrebbero soddisfare:

  1. Identità sinistra: unità (x) .chain (f) ==== f (x)
  2. Identità corretta: m.chain (unità) ==== m
  3. Associatività: m.chain (f) .chain (g) ==== m.chain (x => f (x) .chain (g))

Le leggi sull'identità

Identità sinistra e destra

Una monade è un funzione. Un funzione è un morfismo tra le categorie, A -> B. Il morfismo è rappresentato da una freccia. Oltre alla freccia che vediamo esplicitamente tra gli oggetti, ogni oggetto in una categoria ha anche una freccia su se stessa. In altre parole, per ogni oggetto X in una categoria, esiste una freccia X -> X. Quella freccia è conosciuta come la freccia dell'identità ed è di solito disegnata come una piccola freccia circolare che punta da un oggetto e ritorna allo stesso oggetto .

Morfismi di identità

Associatività

L'associatività significa solo che non importa dove mettiamo la parentesi quando componiamo. Ad esempio, se stai aggiungendo, a + (b + c) è uguale a (a + b) + c. Lo stesso vale per la composizione delle funzioni: (f ∘ g) ∘ h = f ∘ (g ∘ h).

Lo stesso vale per la composizione Kleisli. Devi solo leggerlo al contrario. Quando vedi l'operatore composizione (catena), pensa dopo:

h (x) .chain (x => g (x) .chain (f)) ==== (h (x) .chain (g)). chain (f)

Dimostrare le leggi della monade

Dimostriamo che la monade identitaria soddisfa le leggi della monade:

{// Monade identitaria
  const Id = value => ({
    // Mappatura di Functor
    // Conserva il wrapping per .map () di
    // passa il valore mappato nel tipo
    // sollevamento:
    mappa: f => Id.of (f (valore)),
    // Concatenamento della monade
    // Elimina un livello di avvolgimento
    // omettendo l'ascensore di tipo .of ():
    catena: f => f (valore),
    // Solo un modo conveniente per ispezionare
    // i valori:
    toString: () => `Id ($ {value})`
  });
  // L'ascensore di tipo per questa monade è giusto
  // un riferimento alla fabbrica.
  Id.of = Id;
  const g = n => Id (n + 1);
  const f = n => Id (n * 2);
  // Identità sinistra
  // unit (x) .chain (f) ==== f (x)
  trace ("Id monad left identity") ([
    Id (x) .chain (f),
    f (x)
  ]);
  // Id monade left Identity: Id (40), Id (40)
  // Giusta identità
  // m.chain (unità) ==== m
  trace ("Id monad right identity") ([
    Id (x) .chain (Id.of),
    Id (x)
  ]);
  // Id monad right identity: Id (20), Id (20)
  // Associatività
  // m.chain (f) .chain (g) ====
  // m.chain (x => f (x) .chain (g)
  trace ("Id monad associativity") ([
    Id (x) .chain (g) .chain (f),
    Id (x) .chain (x => g (x) .chain (f))
  ]);
  // Associatività monade id: Id (42), Id (42)
}

Conclusione

Le monadi sono un modo per comporre le funzioni di sollevamento del tipo: g: a => M (b), f: b => M (c). Per fare ciò, le monadi devono appiattire M (b) a b prima di applicare f (). In altre parole, i funzione sono cose su cui è possibile mappare. Le monadi sono cose che puoi flatMap su:

  • Mappa delle funzioni: a => b
  • Mappa di Functor con contesto: Functor (a) => Functor (b)
  • Monadi appiattiti e mappa con contesto: Monade (Monade (a)) => Monade (b)

Una monade si basa su una semplice simmetria - Un modo per avvolgere un valore in un contesto e un modo per scartare il valore dal contesto:

  • Lift / Unit: un tipo di ascensore da un certo tipo nel contesto della monade: a => M (a)
  • Appiattisci / Unisci: non sposta il tipo dal contesto: M (a) => a

E poiché le monadi sono anche funzioni, possono anche mappare:

  • Mappa: Mappa con contesto conservato: M (a) -> M (b)

Combina l'appiattimento con la mappa e otterrai la composizione della funzione catena per le funzioni di sollevamento, nota anche come composizione Kleisli:

  • FlatMap / Chain Flatten + map: M (M (a)) => M (b)

Le monadi devono soddisfare tre leggi (assiomi), note collettivamente come leggi delle monadi:

  • Identità sinistra: unità (x) .chain (f) ==== f (x)
  • Identità corretta: m.chain (unità) ==== m
  • Associatività: m.chain (f) .chain (g) ==== m.chain (x => f (x) .chain (g)

Esempi di monadi che potresti incontrare ogni giorno nel codice JavaScript includono promesse e osservabili. La composizione di Kleisli ti consente di comporre la logica del flusso di dati senza preoccuparti dei dettagli dell'API del tipo di dati e senza preoccuparti dei possibili effetti collaterali, ramificazione condizionale o altri dettagli dei calcoli da scartare nascosti nell'operazione chain ().

Questo rende le monadi uno strumento molto potente per semplificare il tuo codice. Non devi capire o preoccuparti di ciò che sta accadendo all'interno delle monadi per raccogliere i vantaggi semplificativi che le monadi possono offrire, ma ora che sai di più su ciò che c'è sotto il cofano, dare una sbirciatina sotto il cofano non è una prospettiva così spaventosa .

Non c'è bisogno di temere la maledizione di Lady Monadgreen.

Aumenta le tue abilità con il tutoraggio live 1: 1

DevAnywhere è il modo più veloce per salire di livello con competenze JavaScript avanzate:

  • Lezioni dal vivo
  • Orari flessibili
  • Tutoraggio 1: 1
  • Crea app di produzione reali
https://devanywhere.io/

Eric Elliott è autore di "Programmazione di applicazioni JavaScript" (O’Reilly) e cofondatore di DevAnywhere.io. Ha contribuito alle esperienze software per Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC e i migliori artisti della registrazione tra cui Usher, Frank Ocean, Metallica e molti altri.

Lavora dove vuole con la donna più bella del mondo.