Les monades sont-elles une alternative viable (peut-être préférable) aux hiérarchies d'héritage?

20

Je vais utiliser une description indépendante du langage de monades comme celle-ci, décrivant d'abord les monoïdes:

Un monoïde est (grosso modo) un ensemble de fonctions qui prennent un certain type en paramètre et renvoient le même type.

Une monade est (à peu près) un ensemble de fonctions qui prennent un type d' encapsuleur comme paramètre et renvoie le même type d'encapsuleur.

Notez que ce sont des descriptions, pas des définitions. N'hésitez pas à attaquer cette description!

Donc, dans un langage OO, une monade permet des compositions d'opérations comme:

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

Notez que la monade définit et contrôle la sémantique de ces opérations, plutôt que la classe contenue.

Traditionnellement, dans un langage OO, nous utilisions une hiérarchie de classes et un héritage pour fournir ces sémantiques. Nous aurions donc une Birdclasse avec des méthodes takeOff(), flyAround()et land(), et Duck en hériterait.

Mais alors nous avons des ennuis avec les oiseaux incapables de voler, car cela penguin.takeOff()échoue. Nous devons recourir au lancer et à la manipulation d'exception.

De plus, une fois que nous disons que Penguin est un Bird, nous rencontrons des problèmes d'héritage multiple, par exemple si nous avons également une hiérarchie de Swimmer.

Essentiellement, nous essayons de classer les classes en catégories (avec des excuses aux gars de la théorie des catégories) et de définir la sémantique par catégorie plutôt qu'en classes individuelles. Mais les monades semblent être un mécanisme beaucoup plus clair pour ce faire que les hiérarchies.

Donc dans ce cas, nous aurions une Flier<T>monade comme l'exemple ci-dessus:

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

... et nous n'instancierions jamais a Flier<Penguin>. Nous pourrions même utiliser le typage statique pour empêcher cela, peut-être avec une interface de marqueur. Ou vérification des capacités d'exécution pour renflouer. Mais vraiment, un programmeur ne devrait jamais mettre un pingouin dans Flier, dans le même sens, il ne devrait jamais diviser par zéro.

En outre, il est plus généralement applicable. Un dépliant ne doit pas nécessairement être un oiseau. Par exemple Flier<Pterodactyl>, ou Flier<Squirrel>, sans changer la sémantique de ces types individuels.

Une fois que nous classons la sémantique par fonctions composables sur un conteneur - au lieu de hiérarchies de types - cela résout les anciens problèmes avec les classes qui "font, ne font pas" rentrent dans une hiérarchie particulière. Il permet également facilement et clairement plusieurs sémantiques pour une classe, comme Flier<Duck>ainsi que Swimmer<Duck>. Il semble que nous ayons lutté contre un décalage d'impédance en classant les comportements avec les hiérarchies de classes. Les monades le gèrent avec élégance.

Ma question est donc, de la même manière que nous en sommes venus à privilégier la composition à l'héritage, est-il également logique de privilégier les monades à l'héritage?

(BTW je ne savais pas si cela devrait être ici ou dans Comp Sci, mais cela ressemble plus à un problème de modélisation pratique. Mais c'est peut-être mieux là-bas.)

Rob
la source
1
Je ne suis pas sûr de comprendre comment cela fonctionne: un écureuil et un canard ne volent pas de la même manière - donc l'action "voler" doit être implémentée dans ces classes ... Et le dépliant a besoin d'une méthode pour faire l'écureuil et le canard voler ... Peut-être dans une interface Flier commune ... Oups attendez une minute ... Ai-je raté quelque chose?
assylias
Les interfaces sont différentes de l'héritage de classe, car les interfaces définissent les capacités tandis que l'héritage fonctionnel définit le comportement réel. Même en "composition sur héritage", la définition des interfaces reste un mécanisme important (par exemple le polymorphisme). Les interfaces ne rencontrent pas les mêmes problèmes d'héritage multiples. De plus, chaque dépliant pourrait fournir (via une interface et un polymorphisme) des propriétés telles que "getFlightSpeed ​​()" ou "getManuverability ()" pour le conteneur à utiliser.
Rob
3
Essayez-vous de vous demander si l'utilisation du polymorphisme paramétrique est toujours une alternative viable au polymorphisme de sous-type?
ChaosPandion
yup, avec la ride d'ajouter des fonctions composables qui préservent la sémantique. Les types de conteneurs paramétrés existent depuis longtemps, mais en eux-mêmes, ils ne me semblent pas être une réponse complète. C'est pourquoi je me demande si le modèle de monade a un rôle plus fondamental à jouer.
Rob
6
Je ne comprends pas votre description des monoïdes et des monades. La propriété clé des monoïdes est qu'elle implique une opération binaire associative (pensez à l'addition en virgule flottante, à la multiplication d'entiers ou à la concaténation de chaînes). Une monade est une abstraction qui prend en charge le séquençage de divers calculs (éventuellement dépendants) dans un certain ordre.
Rufflewind

Réponses:

15

La réponse courte est non , les monades ne sont pas une alternative aux hiérarchies d'héritage (également connues sous le nom de polymorphisme de sous-type). Vous semblez décrire le polymorphisme paramétrique , que les monades utilisent mais ne sont pas la seule chose à faire.

Pour autant que je les comprenne, les monades n'ont essentiellement rien à voir avec l'héritage. Je dirais que les deux choses sont plus ou moins orthogonales: elles sont destinées à répondre à des problèmes différents, et donc:

  1. Ils peuvent être utilisés en synergie dans au moins deux sens:
    • consultez la Typeclassopedia , qui couvre de nombreuses classes de types de Haskell. Vous remarquerez qu'il existe entre eux des relations de type héritage. Par exemple, Monad descend de Applicative qui est lui-même descendu de Functor.
    • les types de données qui sont des instances de Monads peuvent participer aux hiérarchies de classes. N'oubliez pas que Monad ressemble plus à une interface - l'implémenter pour un type donné vous indique certaines choses sur le type de données, mais pas tout.
  2. Essayer d'utiliser l'un pour faire l'autre sera difficile et moche.

Enfin, bien que cela soit tangentiel à votre question, vous pourriez être intéressé d'apprendre que les monades ont des façons incroyablement puissantes de composer; lisez les transformateurs monades pour en savoir plus. Cependant, c'est toujours un domaine de recherche actif parce que nous (et par nous, je veux dire des gens 100000x plus intelligents que moi) n'avons pas trouvé de bons moyens de composer des monades, et il semble que certaines monades ne composent pas arbitrairement.


Maintenant, pour répondre à votre question (désolé, j'ai l'intention que cela soit utile, et non pour vous faire sentir mal): Je pense qu'il y a beaucoup de prémisses douteuses sur lesquelles j'essaierai d'éclairer.

  1. Une monade est un ensemble de fonctions qui prennent un type de conteneur comme paramètre et renvoie le même type de conteneur.

    Non, c'est en Haskell: un type paramétrés avec une mise en œuvre et , répondant aux lois suivantes:Monadm areturn :: a -> m a(>>=) :: m a -> (a -> m b) -> m b

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

    Il existe certaines instances de Monad qui ne sont pas des conteneurs ( (->) b), et certains conteneurs qui ne sont pas (et ne peuvent pas être créés) des instances de Monad ( Set, en raison de la contrainte de classe de type). Donc, l'intuition «conteneur» est mauvaise. Voir ceci pour plus d'exemples.

  2. Donc, dans un langage OO, une monade permet des compositions d'opérations comme:

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

    Non pas du tout. Cet exemple ne nécessite pas de Monade. Tout ce dont il a besoin, c'est de fonctions avec des types d'entrée et de sortie correspondants. Voici une autre façon de l'écrire qui souligne qu'il s'agit simplement d'une application de fonction:

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

    Je crois que c'est un modèle connu comme une "interface fluide" ou "chaînage de méthode" (mais je ne suis pas sûr).

  3. Notez que la monade définit et contrôle la sémantique de ces opérations, plutôt que la classe contenue.

    Les types de données qui sont également des monades peuvent (et presque toujours!) Avoir des opérations qui ne sont pas liées aux monades. Voici un exemple Haskell composé de trois fonctions []qui n'ont rien à voir avec les monades: []"définit et contrôle la sémantique de l'opération" et la "classe contenue" ne le fait pas, mais ce n'est pas suffisant pour faire une monade:

    \predicate -> length . filter predicate . reverse
    
  4. Vous avez correctement noté qu'il y a des problèmes avec l'utilisation des hiérarchies de classes pour modéliser les choses. Cependant, vos exemples ne présentent aucune preuve que les monades peuvent:

    • Faites un bon travail dans ce domaine que l'héritage est bon
    • Faites un bon travail dans ce domaine où l'héritage est mauvais
Communauté
la source
3
Je vous remercie! Beaucoup de choses à traiter pour moi. Je ne me sens pas mal - j'apprécie grandement la perspicacité. Je me sentirais pire en portant les mauvaises idées. :) (Va au point entier de stackexchange!)
Rob
1
@RobY Vous êtes les bienvenus! Soit dit en passant, si vous n'en avez jamais entendu parler auparavant, je recommande LYAH car c'est une excellente source d'apprentissage des monades (et Haskell!) Car il a des tonnes d'exemples (et je pense que faire des tonnes d'exemples est le meilleur moyen de s'attaquer aux monades).
Il y a beaucoup ici; Je ne veux pas submerger les commentaires, mais quelques commentaires: # 2 land(flyAround(takeOff(new Flier<Duck>(duck))))ne fonctionne pas (au moins dans OO) parce que cette construction nécessite de rompre l'encapsulation pour obtenir les détails de Flier. En enchaînant les opinions sur la classe, les détails de Flier restent cachés et il peut conserver sa sémantique. C'est similaire à la raison pour laquelle dans Haskell une monade est liée (a, M b)et non pas (M a, M b)pour que la monade n'ait pas à exposer son état à la fonction "action".
Rob
# 1, malheureusement, j'essaie de brouiller la définition stricte de Monad dans Haskell, car mapper quoi que ce soit à Haskell a un gros problème: la composition des fonctions, y compris la composition sur les constructeurs , que vous ne pouvez pas faire facilement dans un langage piéton comme Java. Devient donc unit(principalement) un constructeur sur le type contenu, et binddevient (principalement) une opération implicite de compilation (c'est-à-dire une liaison anticipée) qui lie les fonctions "action" à la classe. Si vous avez des fonctions de première classe ou une classe Function <A, Monad <B>>, une bindméthode peut effectuer une liaison tardive, mais je prendrai cet abus ensuite. ;)
Rob
# 3 d'accord, et c'est la beauté de celui-ci. S'il Flier<Thing>contrôle la sémantique du vol, il peut exposer un grand nombre de données et d'opérations qui maintiennent la sémantique du vol, tandis que la sémantique spécifique à la "monade" vise vraiment à la rendre enchaînable et encapsulée. Ces préoccupations pourraient ne pas (et avec celles que j'utilise, ne le sont pas) les préoccupations de la classe à l'intérieur de la monade: par exemple, Resource<String>a une propriété httpStatus, mais pas String.
Rob
1

Ma question est donc, de la même manière que nous en sommes venus à privilégier la composition à l'héritage, est-il également logique de privilégier les monades à l'héritage?

Dans les langues non OO, oui. Dans les langues OO plus traditionnelles, je dirais non.

Le problème est que la plupart des langues n'ont pas de spécialisation de type, ce qui signifie que vous ne pouvez pas créer Flier<Squirrel>et Flier<Bird>avoir différentes implémentations. Vous devez faire quelque chose comme static Flier Flier::Create(Squirrel)(puis surcharger pour chaque type). Ce qui signifie à son tour que vous devez modifier ce type chaque fois que vous ajoutez un nouvel animal, et probablement dupliquer un peu de code pour le faire fonctionner.

Oh, et dans quelques langues (C # par exemple), public class Flier<T> : T {}c'est illégal. Il ne construira même pas. La plupart, sinon tous les programmeurs OO s'attendraient Flier<Bird>à être toujours un Bird.

Telastyn
la source
Merci pour le commentaire. J'ai d'autres réflexions, mais simplement, même s'il Flier<Bird>s'agit d'un conteneur paramétré, personne ne le considérerait comme un Bird(!?) List<String>Est une liste et non une chaîne.
Rob
@RobY - Fliern'est pas seulement un conteneur. Si vous ne le considérez que comme un conteneur, pourquoi penseriez-vous qu'il pourrait remplacer l'utilisation de l'héritage?
Telastyn
Je vous ai perdu là-bas ... mon point est que la monade est un conteneur amélioré. Animal / Bird / Penguinest généralement un mauvais exemple, car il apporte toutes sortes de sémantiques. Un exemple pratique est une monade REST-ish que nous utilisons: Resource<String>.from(uri).get() Resourceajoute de la sémantique sur String(ou un autre type), donc ce n'est évidemment pas un String.
Rob
@RobY - mais il n'est pas non plus lié à l'héritage.
Telastyn
Sauf que c'est un autre type de confinement. Je peux mettre String dans Resource, ou je pourrais résumer une classe ResourceString et utiliser l'héritage. Ma pensée est que mettre une classe dans un conteneur de chaînage est un meilleur moyen d'abstraire un comportement que de le placer dans une hiérarchie de classes avec héritage. Donc "sans aucun lien" dans le sens de "remplacer / éviter" - oui.
Rob