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 Bird
classe 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.)
Réponses:
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:
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.
Non, c'est en Haskell: un type paramétrés avec une mise en œuvre et , répondant aux lois suivantes:
Monad
m a
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
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.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:
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).
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: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:
la source
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".unit
(principalement) un constructeur sur le type contenu, etbind
devient (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>>, unebind
méthode peut effectuer une liaison tardive, mais je prendrai cet abus ensuite. ;)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.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>
etFlier<Bird>
avoir différentes implémentations. Vous devez faire quelque chose commestatic 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'attendraientFlier<Bird>
à être toujours unBird
.la source
Flier<Bird>
s'agit d'un conteneur paramétré, personne ne le considérerait comme unBird
(!?)List<String>
Est une liste et non une chaîne.Flier
n'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?Animal / Bird / Penguin
est 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()
Resource
ajoute de la sémantique surString
(ou un autre type), donc ce n'est évidemment pas unString
.