Le monadi sono un'alternativa praticabile (forse preferibile) alle gerarchie di ereditarietà?

17

Userò una descrizione di monade simile alla lingua, come questa, descrivendo prima i monoidi:

A monoid is (roughly) a set of functions that take some type as a parameter and return the same type.

A monad is (roughly) a set of functions that take a wrapper type as a parameter and returns the same wrapper type.

Nota quelle 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 quelle operazioni, piuttosto che la classe contenuta.

Tradizionalmente, in un linguaggio OO useremmo una gerarchia di classi & eredità per fornire quella semantica. Avremmo quindi una classe Bird con metodi takeOff() , flyAround() e land() , e Duck li erediterebbe.

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

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

Essenzialmente stiamo cercando di mettere le classi in categorie (con scuse ai ragazzi di Theory Theory), e definire la semantica per categoria piuttosto che in classi individuali. Ma le monadi sembrano un meccanismo molto più chiaro per farlo rispetto alle gerarchie.

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

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

... e non creeremo mai un'istanza di Flier<Penguin> . Potremmo anche usare la digitazione statica per evitare che ciò accada, magari con un'interfaccia marcatore. Oppure funzionalità di runtime: controllo del salvataggio. Ma in realtà, un programmatore non dovrebbe mai mettere un pinguino in un Flier, nello stesso senso in cui non dovrebbero mai dividere per zero.

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

Una volta che classifichiamo la semantica per funzioni componibili su un contenitore, anziché con le gerarchie di tipi, risolve i vecchi problemi con classi che "tipo di azione, tipo di non" si adattano a una particolare gerarchia. Anche facilmente & consente chiaramente più semantiche per una classe, come Flier<Duck> e Swimmer<Duck> . Sembra che abbiamo lottato con un disadattamento di impedenza classificando il comportamento con le gerarchie di classi. I Monad lo gestiscono con eleganza.

Quindi la mia domanda è, nello stesso modo in cui siamo arrivati a favorire la composizione rispetto all'ereditarietà, ha anche senso favorire le monadi sull'ereditarietà?

(BTW non ero sicuro se questo dovrebbe essere qui o in Comp Sci, ma questo sembra più un problema di modellazione pratica, ma forse è meglio lì.)

    
posta Rob 03.04.2014 - 01:29
fonte

2 risposte

14

La risposta breve è no , le monadi non sono un'alternativa alle gerarchie di ereditarietà (noto anche come polimorfismo di sottotipo). Sembra che tu stia descrivendo il polimorfismo parametrico , che le monadi usano ma non sono l'unica cosa da fare.

Per quanto li comprendo, le monadi non hanno sostanzialmente nulla a che fare con l'ereditarietà. Direi che le due cose sono più o meno ortogonali: hanno lo scopo di affrontare diversi problemi, e così:

  1. Possono essere usati sinergicamente in almeno due sensi:
    • dai un'occhiata a la Typeclassopedia , che copre molte delle classi di tipi di Haskell. Noterai che ci sono relazioni di tipo ereditario tra loro. Ad esempio, Monad discende da Applicative che discende esso stesso da Functor.
    • i tipi di dati che sono istanze di Monade 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 su tutto.
  2. Cercare di usarne uno per fare l'altro sarà difficile e brutto.

Infine, anche se questo è tangenziale alla tua domanda, potresti essere interessato a sapere che le monadi hanno alcuni incredibilmente potenti modi di comporre; leggere su trasformatori monad per saperne di più. Tuttavia, questa è ancora un'area di ricerca attiva perché noi (e per noi, intendo persone 100000x più intelligenti di me) non abbiamo trovato grandi modi per comporre le monadi, e sembra che alcune monadi non compongano arbitrariamente.

Ora, scegli la tua domanda (scusami, intendo che ciò sia di aiuto e non ti faccia sentire male): sento che ci sono molte premesse discutibili su cui cercherò di fare luce.

  1. A monad is a set of functions that take a container type as a parameter and returns the same container type.

    No, questo è Monad in Haskell : un tipo parametrico m a con un'implementazione di return :: a -> m a e (>>=) :: 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 contenitori ( (->) b ), e ci sono alcuni contenitori che non sono (e non possono essere creati) istanze di Monad ( Set , a causa del vincolo della classe di caratteri). Quindi l'intuizione del "contenitore" è scarsa. Vedi questo per ulteriori esempi.

  2. So in an OO language, a monad permits operation compositions like:

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

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

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

    Credo che questo sia un pattern noto come "interfaccia fluente" o "metodo di concatenamento" (ma non ne sono sicuro)

  3. Note that the monad defines and controls the semantics of those operations, rather than the contained class.

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

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

    • Fai un buon lavoro con quella roba che l'ereditarietà è buona
    • Fai un buon lavoro con quella roba che l'ereditarietà è cattiva a
risposta data 06.04.2014 - 01:40
fonte
1

So my question is, in the same way that we've come to favor composition over inheritance, does it also make sense to favor monads over inheritance?

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 puoi fare Flier<Squirrel> e Flier<Bird> avere implementazioni differenti. Devi fare qualcosa come static Flier Flier::Create(Squirrel) (e poi sovraccaricare per ogni tipo). Il che a sua volta significa che devi modificare questo tipo ogni volta che aggiungi un nuovo animale e probabilmente duplichi un bel po 'di codice per farlo funzionare.

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

    
risposta data 03.04.2014 - 16:48
fonte

Leggi altre domande sui tag