Le monadi sono un'alternativa praticabile (forse preferibile) alle gerarchie ereditarie?


20

Userò una descrizione indipendente dal linguaggio di monadi come questa, descrivendo prima i monoidi:

Un monoide è (approssimativamente) un insieme di funzioni che accettano un tipo come parametro e restituiscono lo stesso tipo.

Una monade è (approssimativamente) un insieme di funzioni che accettano un tipo di wrapper come parametro e restituiscono lo stesso tipo di wrapper.

Nota che sono descrizioni, non definizioni. Sentiti libero di attaccare quella descrizione!

Quindi in un linguaggio OO, una monade consente composizioni operative come:

Flier<Duck> m = new Flier<Duck>(duck).takeOff().flyAround().land()

Si noti che la monade definisce e controlla la semantica di tali operazioni, piuttosto che la classe contenuta.

Tradizionalmente, in un linguaggio OO useremmo una gerarchia di classi ed ereditarietà per fornire quella semantica. Così avremmo una Birdclasse con metodi takeOff(), flyAround()e land(), e anatra avrebbero ereditato quelli.

Ma poi ci mettiamo nei guai con gli uccelli incapaci di volare, perché penguin.takeOff()falliscono. Dobbiamo ricorrere al lancio e alla gestione delle eccezioni.

Inoltre, una volta che diciamo che Penguin è un Bird, ci imbattiamo in problemi con eredità multipla, ad esempio se abbiamo anche una gerarchia di Swimmer.

In sostanza stiamo provando a mettere le classi in categorie (con scuse per i ragazzi della teoria delle categorie) e definire la semantica per categoria piuttosto che nelle singole classi. Ma le monadi sembrano un meccanismo molto più chiaro per farlo rispetto alle gerarchie.

Quindi, in questo caso, avremmo una Flier<T>monade come nell'esempio sopra:

Flier<Duck> m = new Flier<Duck>(duck).takeOff().flyAround().land()

... e non avremmo mai un'istanza a Flier<Penguin>. Potremmo anche usare la tipizzazione statica per evitare che ciò accada, magari con un'interfaccia marcatore. O il controllo delle capacità di runtime per il salvataggio. Ma davvero, un programmatore non dovrebbe mai mettere un Pinguino in Flier, nello stesso senso in cui non dovrebbe mai dividere per zero.

Inoltre, è più generalmente applicabile. Un volantino non deve essere un uccello. Ad esempio Flier<Pterodactyl>, oppure Flier<Squirrel>, senza modificare la semantica di quei singoli tipi.

Una volta classificata la semantica in base a funzioni componibili su un contenitore - anziché con gerarchie di tipi - risolve i vecchi problemi con le classi che "tipo di fare, tipo di non" rientrano in una particolare gerarchia. Inoltre, consente facilmente e chiaramente più semantiche per una classe, come Flier<Duck>pure Swimmer<Duck>. Sembra che abbiamo lottato con una discrepanza di impedenza classificando il comportamento con le gerarchie di classi. Le monadi lo gestiscono elegantemente.

Quindi la mia domanda è, allo stesso modo in cui siamo arrivati ​​a favorire la composizione sull'eredità, ha senso anche favorire le monadi sull'eredità?

(A proposito non ero sicuro se questo dovesse essere qui o in Comp Sci, ma questo sembra più un problema pratico di modellazione. Ma forse è meglio laggiù.)


1
Non sono sicuro di capire come funziona: uno scoiattolo e un'anatra non volano allo stesso modo - quindi "l'azione di volo" deve essere implementata in quelle classi ... E il volantino ha bisogno di un metodo per rendere lo scoiattolo e l'anatra volare ... Forse in un'interfaccia comune di Flier ... Oops aspetta un minuto ... Mi sono perso qualcosa?
assylias,

Le interfacce sono diverse dall'ereditarietà delle classi, poiché le interfacce definiscono le capacità mentre l'ereditarietà funzionale definisce il comportamento effettivo. Anche nella "composizione per ereditarietà", la definizione delle interfacce è ancora un meccanismo importante (ad esempio il polimorfismo). Le interfacce non incontrano gli stessi problemi di ereditarietà multipla. Inoltre, ogni volantino potrebbe fornire (tramite un'interfaccia e un polimorfismo) proprietà di capacità come "getFlightSpeed ​​()" o "getManuverability ()" che il contenitore può usare.
Rob

3
Stai cercando di chiederti se l'uso del polimorfismo parametrico sia sempre una valida alternativa al polimorfismo del sottotipo?
ChaosPandion,

sì, con la ruga di aggiungere funzioni componibili che preservano la semantica. I tipi di container con parametri sono in circolazione da molto tempo, ma da soli non mi sembrano una risposta completa. Ecco perché mi chiedo se il modello della monade abbia un ruolo più fondamentale da svolgere.
Rob

6
Non capisco la tua descrizione di monoidi e monadi. La proprietà chiave dei monoidi è che comporta un'operazione binaria associativa (si pensi all'aggiunta in virgola mobile, alla moltiplicazione di numeri interi o alla concatenazione di stringhe). Una monade è un'astrazione che supporta il sequenziamento di vari calcoli (possibilmente dipendenti) in un certo ordine.
Rufflewind

Risposte:


15

La risposta breve è no , le monadi non sono un'alternativa alle gerarchie ereditarie (noto anche come polimorfismo dei sottotipi). Sembra che tu stia descrivendo il polimorfismo parametrico , di cui le monadi si avvalgono ma non sono l'unica cosa per farlo.

Per quanto li capisco, le monadi non hanno essenzialmente nulla a che fare con l'eredità. Direi che le due cose sono più o meno ortogonali: hanno lo scopo di affrontare diversi problemi, e quindi:

  1. Possono essere usati sinergicamente in almeno due sensi:
    • controlla la Typeclassopedia , che copre molte delle classi di tipi di Haskell. Noterai che ci sono relazioni simili all'eredità tra di loro. Ad esempio, Monade è discendente da Applicativo che è esso stesso discendente da Functor.
    • i tipi di dati che sono istanze di Monadi possono partecipare alle gerarchie di classi. Ricorda, Monad è più simile a un'interfaccia: implementarla per un determinato tipo ti dice alcune cose sul tipo di dati, ma non tutto.
  2. Cercare di usare l'uno per fare l'altro sarà difficile e brutto.

Infine, sebbene ciò sia tangenziale alla tua domanda, potresti essere interessato a sapere che le monadi hanno dei modi incredibilmente potenti per comporre; leggi sui trasformatori di monade per saperne di più. Tuttavia, questa è ancora un'area attiva di ricerca perché noi (e per noi, intendo le persone 100000 volte più intelligenti di me) non abbiamo trovato ottimi modi per comporre le monadi, e sembra che alcune monadi non compongano arbitrariamente.


Ora, per scegliere la tua domanda (scusami, intendo che ciò sia utile e non ti faccia stare male): Sento che ci sono molte premesse discutibili su cui cercherò di far luce.

  1. Una monade è un insieme di funzioni che accettano un tipo di contenitore come parametro e restituiscono lo stesso tipo di contenitore.

    No, questo è Monadin Haskell: un tipo m acon parametri con un'implementazione di return :: a -> m ae (>>=) :: m a -> (a -> m b) -> m b, che soddisfa le seguenti leggi:

    return a >>= k  ==  k a
    m >>= return  ==  m
    m >>= (\x -> k x >>= h)  ==  (m >>= k) >>= h
    

    Ci sono alcune istanze di Monad che non sono container ( (->) b) e ci sono alcuni contenitori che non sono (e non possono essere create) istanze di Monad ( Set, a causa del vincolo di classe del tipo). Quindi l'intuizione "contenitore" è scarsa. Vedi questo per altri esempi.

  2. Quindi in un linguaggio OO, una monade consente composizioni operative come:

      Flier<Duck> m = new Flier<Duck>(duck).takeOff().flyAround().land()
    

    No, per niente. Quell'esempio non richiede una Monade. Tutto ciò che serve sono funzioni con tipi di input e output corrispondenti. Ecco un altro modo di scriverlo che sottolinea che si tratta solo di un'applicazione di funzioni:

    Flier<Duck> m = land(flyAround(takeOff(new Flier<Duck>(duck))));
    

    Credo che questo sia uno schema noto come "interfaccia fluente" o "concatenamento di metodi" (ma non ne sono sicuro).

  3. Si noti che la monade definisce e controlla la semantica di tali operazioni, piuttosto che la classe contenuta.

    I tipi di dati che sono anche monadi possono (e quasi sempre lo fanno!) Avere operazioni non correlate alle monadi. Ecco un esempio di Haskell composto da tre funzioni sulle []quali non ha nulla a che fare con le monadi: []"definisce e controlla la semantica dell'operazione" e la "classe contenuta" no, ma non è sufficiente per creare una monade:

    \predicate -> length . filter predicate . reverse
    
  4. Hai notato correttamente che ci sono problemi con l'uso delle gerarchie di classi per modellare le cose. Tuttavia, i tuoi esempi non presentano alcuna prova che le monadi possano:

    • Fai un buon lavoro in quelle cose in cui l'eredità è brava
    • Fai un buon lavoro in quella roba in cui l'eredità è cattiva

3
Grazie! Molto da elaborare. Non mi sento male - apprezzo molto l'intuizione. Mi sentirei peggio portando in giro le cattive idee. :) (Va a tutto il punto di scambio di stack!)
Rob

1
@RobY Prego! A proposito, se non ne hai mai sentito parlare prima, consiglio LYAH in quanto è un'ottima fonte per l'apprendimento delle monadi (e Haskell!) Perché ha tonnellate di esempi (e sento che fare tonnellate di esempi è il modo migliore per affrontare monadi).

Ci sono molti qui; Non voglio sommergere i commenti, ma alcuni commenti: # 2 land(flyAround(takeOff(new Flier<Duck>(duck))))non funziona (almeno in OO) perché quella costruzione richiede l'incapsulamento di rottura per ottenere i dettagli di Flier. Concatenando le operazioni sulla classe, i dettagli di Flier rimangono nascosti e può preservare la sua semantica. È simile al motivo per cui in Haskell si lega una monade (a, M b)e non in (M a, M b)modo che la monade non debba esporre il suo stato alla funzione "azione".
Rob,

# 1, sfortunatamente sto cercando di confondere la definizione rigorosa di Monad in Haskell, perché mappare qualsiasi cosa su Haskell ha un grosso problema: composizione di funzioni, inclusa composizione su costruttori , che non si può fare facilmente in un linguaggio pedonale come Java. Quindi unitdiventa (principalmente) costruttore sul tipo contenuto e binddiventa (principalmente) un'operazione di tempo di compilazione implicita (ovvero associazione anticipata) che lega le funzioni di "azione" alla classe. Se hai funzioni di prima classe, o una classe Funzione <A, Monade <B>>, un bindmetodo può eseguire l'associazione tardiva, ma prenderò poi l'abuso. ;)
Rob

# 3 sono d'accordo, e questa è la bellezza. Se Flier<Thing>controlla la semantica del volo, può esporre un sacco di dati e operazioni che mantengono la semantica del volo, mentre la semantica specifica della "monade" si occupa solo di renderla incatenabile e incapsulata. Quelle preoccupazioni potrebbero non (e con quelle che ho usato, non lo sono) preoccupazioni della classe all'interno della monade: ad esempio Resource<String>ha una proprietà httpStatus, ma String no.
Rob,

1

Quindi la mia domanda è, allo stesso modo in cui siamo arrivati ​​a favorire la composizione sull'eredità, ha senso anche favorire le monadi sull'eredità?

In lingue non OO, sì. Nelle lingue OO più tradizionali, direi di no.

Il problema è che la maggior parte delle lingue non ha specializzazione di tipo, il che significa che non è possibile effettuare Flier<Squirrel>e Flier<Bird>avere implementazioni diverse. Devi fare qualcosa di simile static Flier Flier::Create(Squirrel)(e quindi sovraccaricare per ogni tipo). Il che a sua volta significa che devi modificare questo tipo ogni volta che aggiungi un nuovo animale e probabilmente duplicare un bel po 'di codice per farlo funzionare.

Oh, e in non poche lingue (ad esempio C #) public class Flier<T> : T {}è illegale. Non costruirà nemmeno. La maggior parte, se non tutti i programmatori OO, si aspetterebbero Flier<Bird>di essere ancora un Bird.


grazie per il commento. Ho qualche idea in più, ma solo banalmente, anche se Flier<Bird>è un contenitore con parametri, nessuno lo considererebbe un Bird(!?) List<String>Un Elenco non una Stringa.
Rob,

@RobY - Fliernon è solo un contenitore. Se lo consideri solo un contenitore, perché mai pensi che potrebbe sostituire l'uso dell'eredità?
Telastyn,

Ti ho perso lì ... il mio punto è che la monade è un contenitore potenziato. Animal / Bird / Penguindi solito è un cattivo esempio, perché introduce ogni sorta di semantica. Un esempio pratico è una monade REST-ish che stiamo usando: Resource<String>.from(uri).get() Resourceaggiunge la semantica sopra String(o qualche altro tipo), quindi ovviamente non è una String.
Rob

@RobY - ma poi non è in alcun modo correlato all'eredità.
Telastyn,

Solo che è un diverso tipo di contenimento. Posso mettere String in Resource o posso astrarre una classe ResourceString e usare l'ereditarietà. Il mio pensiero è che mettere una classe in un contenitore concatenato sia un modo migliore per astrarre il comportamento piuttosto che metterlo in una gerarchia di classi con eredità. Quindi "nessuna relazione" nel senso di "sostituzione / eliminazione" - sì.
Rob,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.