Différence entre les interfaces OOP et les classes de type FP

Duplication possible:
Interface de Java et classe de type Haskell: différences et similitudes?

Lorsque j’ai commencé à apprendre le haskell, on m’a dit que les classes de caractères étaient plus puissantes / différentes que les interfaces.

Un an plus tard, j’ai beaucoup utilisé les interfaces et les classes de types et je n’ai pas encore vu d’exemple ou d’explication de leur différence. Ce n’est pas une révélation qui vient naturellement, j’ai manqué quelque chose d’évident ou il n’y a en réalité aucune différence réelle.

La recherche sur Internet n’a pas donné de résultats substantiels. Alors, vous avez la réponse?

Vous pouvez regarder cela sous plusieurs angles. D’autres personnes seront en désaccord, mais je pense que les interfaces OOP sont un bon sharepoint départ pour comprendre les classes de types (comparé à partir de rien du tout).

Les gens aiment souligner que, d’un sharepoint vue conceptuel, les classes de types classent les types, tout comme les ensembles – “l’ensemble des types qui supportent ces opérations, ainsi que d’autres attentes qui ne peuvent pas être codées dans le langage même”. Cela a du sens et est occasionnellement fait pour déclarer une classe de type sans méthode, en disant “ne faites que votre type une instance de cette classe si elle répond à certaines exigences”. Cela arrive très rarement avec les interfaces OOP.

En termes de différences concrètes, les classes de type sont plus puissantes de plusieurs manières que les interfaces OOP:

  • Le plus important est que les classes de types découplent la déclaration selon laquelle un type implémente une interface de la déclaration du type lui-même. Avec les interfaces OOP, vous répertoriez les interfaces implémentées par un type lorsque vous le définissez, et il est impossible d’en append plus tard. Avec les classes de type, si vous créez une nouvelle classe de type qu’un type donné “en haut de la hiérarchie du module” pourrait implémenter mais que vous ne connaissez pas, vous pouvez écrire une déclaration d’instance. Si vous avez un type et une classe de type provenant de tiers distincts qui ne se connaissent pas, vous pouvez écrire une déclaration d’instance. Dans les cas analogues avec les interfaces OOP, vous êtes principalement coincé, même si les langages OOP ont évolué vers des “modèles de conception” (adaptateur) pour contourner la limitation.

  • Le plus important (subjectif bien sûr) est que, conceptuellement, les interfaces OOP sont un ensemble de méthodes qui peuvent être invoquées sur les objects qui implémentent l’interface. Les classes de types sont un ensemble de méthodes qui peuvent être utilisées avec des types membres. de la classe. La distinction est importante. Étant donné que les méthodes de classe de type sont définies par référence au type et non à l’object, il n’y a aucun obstacle à avoir des méthodes avec plusieurs objects du type en tant que parameters (opérateurs d’égalité et de comparaison) ou renvoyant un object du type ( diverses opérations arithmétiques), ou même des constantes du type (minimum et maximum lié). Les interfaces OOP ne peuvent tout simplement pas faire cela, et les langages de la POO ont développé des modèles de conception (par exemple, une méthode de clone virtuel) pour contourner la limitation.

  • Les interfaces OOP ne peuvent être définies que pour les types. Les classes de type peuvent également être définies pour ce qu’on appelle des “constructeurs de types”. Les différents types de collections définis à l’aide de templates et de génériques dans les différents langages OOP dérivés de C sont des constructeurs de types: List prend un type T comme argument et construit le type List . Les classes de type permettent de déclarer des interfaces pour les constructeurs de types: say, une opération de mappage pour les types de collection qui appelle une fonction fournie sur chaque élément d’une collection et collecte les résultats dans une nouvelle copie de la collection – éventuellement avec un type d’élément différent! Encore une fois, vous ne pouvez pas faire cela avec les interfaces OOP.

  • Si un paramètre donné doit implémenter plusieurs interfaces, avec des classes de type, il est très facile de lister celles dont il doit être membre; Avec les interfaces OOP, vous ne pouvez spécifier qu’une seule interface comme type de pointeur ou de référence donné. Si vous en avez besoin pour implémenter davantage, vos seules options sont peu attrayantes, comme l’écriture d’une interface dans la signature et la conversion vers les autres, ou l’ajout de parameters distincts pour chaque interface et la nécessité de pointer vers le même object. Vous ne pouvez même pas le résoudre en déclarant une nouvelle interface vide qui hérite de celles dont vous avez besoin, car un type ne sera pas automatiquement considéré comme implémentant votre nouvelle interface simplement parce qu’il implémente ses ancêtres. (Si vous pouviez déclarer des implémentations après le fait, ce ne serait pas un problème, mais oui, vous ne pouvez pas le faire non plus.)

  • Sorte du cas inverse de celui ci-dessus, vous pouvez exiger que deux parameters aient des types implémentant une interface particulière et qu’ils soient du même type. Avec les interfaces OOP, vous ne pouvez spécifier que la première partie.

  • Les déclarations d’instance pour les classes de type sont plus flexibles. Avec les interfaces OOP, vous pouvez seulement dire “Je déclare un type X, et il implémente l’interface Y”, où X et Y sont spécifiques. Avec les classes de type, vous pouvez dire que “tous les types List dont les types d’élément satisfont à ces conditions sont membres de Y”. (Vous pouvez également dire “tous les types qui sont membres de X et Y sont également membres de Z”, bien que dans Haskell cela pose problème pour un certain nombre de raisons.)

  • Les “contraintes de super-classe” sont plus flexibles que le simple inheritance d’interface. Avec les interfaces OOP, vous pouvez seulement dire “pour qu’un type implémente cette interface, il doit également implémenter ces autres interfaces”. C’est le cas le plus courant avec les classes de type également, mais les contraintes de superclasse vous permettent également de dire des choses comme “SomeTypeConstructor doit implémenter une telle interface” ou “les résultats de cette fonction appliqués au type doivent satisfaire et-so contrainte “, et ainsi de suite.

  • Ceci est actuellement une extension de langage dans Haskell (tout comme les fonctions de type), mais vous pouvez déclarer des classes de types impliquant plusieurs types. Par exemple, une classe d’isomorphisme: la classe des paires de types où vous pouvez convertir sans perte de l’un vers l’autre et vers l’arrière. Encore une fois, impossible avec les interfaces OOP.

  • Je suis sûr qu’il y a plus.

Il est intéressant de noter que dans les langages POO qui ajoutent des génériques, certaines de ces limitations peuvent être effacées (quasortingème, cinquième, éventuellement deuxième point).

De l’autre côté, il y a deux choses importantes que les interfaces OOP peuvent faire et les classes de type nativement ne pas:

  • Envoi dynamic Dans les langages POO, il est sortingvial de faire circuler et de stocker des pointeurs vers un object implémentant une interface, et d’appeler des méthodes à l’exécution qui seront résolues en fonction du type d’exécution dynamic de l’object. En revanche, les contraintes de classe de type sont toutes définies par défaut au moment de la compilation – et peut-être de manière surprenante, dans la grande majorité des cas, c’est tout ce dont vous avez besoin. Si vous avez besoin de dispatch dynamic, vous pouvez utiliser ce que l’on appelle les types existentiels (qui sont actuellement une extension de langage dans Haskell): une construction dans laquelle il “oublie” le type d’un object, et ne se souvient que il obéissait à certaines contraintes de classe de type. À partir de là, il se comporte exactement de la même manière que les pointeurs ou les références à des objects implémentant des interfaces dans les langages de la POO, et les classes de type ne présentent aucun déficit dans ce domaine. (Il est à noter que si vous avez deux existentiels implémentant la même classe de type et une méthode de classe de type qui nécessite deux parameters de ce type, vous ne pouvez pas utiliser les existentiels comme parameters, car vous ne pouvez pas savoir si les existentiels avaient le même type, mais par rapport aux langages de la POO, qui ne peuvent pas avoir de telles méthodes, ce n’est pas une perte.

  • Coulée d’exécution des objects vers les interfaces. Dans les langages POO, vous pouvez prendre un pointeur ou une référence à l’exécution et tester s’il implémente une interface, et le “lancer” sur cette interface si c’est le cas. Les classes de type n’ont rien d’équivalent (ce qui est un avantage à certains égards, car il préserve une propriété appelée «paramésortingsation», mais je n’y entrerai pas ici). Bien sûr, rien ne vous empêche d’append une nouvelle classe de type (ou d’augmenter une classe existante) avec des méthodes permettant de convertir des objects du type en existentials, quel que soit le type de classe souhaité. (Vous pouvez également implémenter une telle fonctionnalité de manière plus générique qu’une bibliothèque, mais elle est considérablement plus complexe. Je prévois de la terminer et de la télécharger sur Hackage un jour , je vous le promets!)

(Je tiens à souligner que bien que vous * puissiez * faire ces choses, beaucoup de gens envisagent d’émuler la POO de cette façon et suggèrent d’utiliser des solutions plus simples, telles que des enregistrements explicites de fonctions plutôt que des classes de type. cette option n’est pas moins puissante.)

Sur le plan opérationnel, les interfaces OOP sont généralement implémentées en stockant un pointeur ou des pointeurs dans l’object lui-même, qui pointent vers des tables de pointeurs de fonction pour les interfaces implémentées par l’object. Les classes de type sont généralement implémentées (pour les langages qui font du polymorphism par boxe, comme Haskell, plutôt que le polymorphism par multi-instanciation, comme C ++) par le “dictionnaire pass”: le compilateur transmet implicitement le pointeur à la table des fonctions ) en tant que paramètre caché pour chaque fonction qui utilise la classe de type, et la fonction obtient une copie quel que soit le nombre d’objects impliqués (c’est pourquoi vous devez faire les choses mentionnées dans le deuxième point ci-dessus). L’implémentation de types existentiels ressemble beaucoup à ce que font les langages OOP: un pointeur vers le dictionnaire de classe de type est stocké avec l’object en tant que «preuve» que le type «oublié» en fait partie.

Si vous avez déjà entendu parler de la proposition de «concepts» pour C ++ (telle que proposée à l’origine pour C ++ 11), il s’agit essentiellement des classes de types d’Haskell réinventées pour les modèles C ++. Je pense parfois que ce serait bien d’avoir un langage qui prend simplement les concepts avec C ++, extrait la moitié des fonctions orientées object et des fonctions virtuelles, nettoie la syntaxe et les autres verrous, et ajoute les types existentiels pour les besoins d’exécution envoi dynamic basé sur le type. (Mise à jour: La rouille est fondamentalement cela, avec beaucoup d’autres belles choses.)

Je suppose que vous parlez des classes de type Haskell. Ce n’est pas vraiment la différence entre les interfaces et les classes de types. Comme son nom l’indique, une classe de type est juste une classe de types avec un ensemble commun de fonctions (et les types associés, si vous activez l’extension TypeFamilies).

Cependant, le système de types de Haskell est en soi plus puissant que, par exemple, le système de type C #. Cela vous permet d’écrire des classes de type dans Haskell, que vous ne pouvez pas exprimer en C #. Même une classe de type aussi simple que Functor ne peut pas être exprimée en C #:

 class Functor f where fmap :: (a -> b) -> fa -> fb 

Le problème avec C # est que les génériques ne peuvent pas être génériques eux-mêmes. En d’autres termes, en C #, seuls les types de type * peuvent être polymorphes. Haskell autorise les constructeurs de types polymorphes, de sorte que les types de tout type peuvent être polymorphes.

C’est la raison pour laquelle de nombreuses fonctions génériques puissantes dans Haskell ( mapM , liftA2 , etc.) ne peuvent pas être exprimées dans la plupart des langues avec un système de type moins puissant.

Voir une réponse “officielle” à http://www.haskell.org/haskellwiki/OOP_vs_type_classes

La principale différence – qui rend les classes de type beaucoup plus flexibles que les interfaces – est que les classes de type sont indépendantes de leurs types de données et peuvent être ajoutées ultérieurement . Une autre différence (au moins en Java) est que vous pouvez fournir des implémentations par défaut. Un exemple:

 //Java public interface HasSize { public int size(); public boolean isEmpty(); } 

Avoir cette interface est bien, mais il n’y a aucun moyen de l’append à une classe existante sans la modifier. Si vous avez de la chance, la classe n’est pas finale (par exemple ArrayList ), vous pouvez donc écrire une sous-classe implémentant l’interface. Si la classe est finale (disons Ssortingng ), vous n’avez pas de chance.

Comparez cela à Haskell. Vous pouvez écrire la classe de type:

 --Haskell class HasSize a where size :: a -> Int isEmpty :: a -> Bool isEmpty x = size x == 0 

Et vous pouvez append des types de données existants à la classe sans les toucher:

 instance HasSize [a] where size = length 

Une autre propriété intéressante des classes de type est l’invocation implicite. Par exemple, si vous avez un Comparator en Java, vous devez le transmettre en tant que valeur explicite. Dans Haskell, l’équivalent peut être utilisé automatiquement dès qu’une instance appropriée est dans la scope.