Question

Classes de types semble être une excellente possibilité d'écrire des fonctions génériques et réutilisables de manière très cohérente, efficace et extensible.Mais reste Non Le « langage courant » les fournit - Au contraire : Concepts, qui sont une idée assez analogique, ont été exclu du prochain C++ !

Quel est le raisonnement contre les classes de types ?Apparemment, de nombreux langages recherchent un moyen de résoudre des problèmes similaires :.NET a introduit des contraintes et des interfaces génériques telles que IComparable qui permettent des fonctions comme

T Max<T>(T a, T b) where T : IComparable<T> { // }

pour fonctionner sur tous les types qui implémentent l’interface.

Scala utilise plutôt une combinaison de caractéristiques et ainsi appelé paramètres implicites/afficher les limites, qui sont automatiquement transmis aux fonctions génériques.

Mais les deux concepts présentés ici présentent de grands inconvénients : les interfaces sont basées sur l'héritage et donc relativement lentes en raison de l'indirection et de plus, il n'y a aucune possibilité de laisser un type existant les implémenter.

Si nous avions besoin d'une abstraction pour un monoïde, nous pourrions très bien écrire une interface et laisser nos types l'implémenter, mais les types intégrés comme int ne pourrait jamais fonctionner sur vos fonctions de manière native.

Les paramètres implicites sont plutôt incompatibles avec les interfaces/traits habituels.

Avec les classes de types, il n'y aurait pas de problème (pseudo-code)

typeclass Monoid of A where
    static operator (+) (x : A, y : A) : A
    static val Zero : A 
end

instance Int of Monoid where
   static operator (+) (x : Int, y : Int) : Int = x + y
   static val Zero : Int = 0
end

Alors pourquoi n'utilisons-nous pas de classes de types ?Ont-ils finalement de sérieux inconvénients ?

Modifier:Veuillez ne pas confondre les classes de types avec le typage structurel, les modèles C++ purs ou le typage canard.Une classe de types est explicitement instancié par types et pas seulement satisfaits par convention.De plus, il peut transporter des implémentations utiles et ne pas simplement définir une interface.

Était-ce utile?

La solution

Des concepts ont été exclus parce que le comité ne pensait pas pouvoir les mettre au point à temps et parce qu'ils n'étaient pas considérés comme essentiels à la sortie.Ce n’est pas qu’ils ne pensent pas que ce soit une bonne idée, ils ne pensent tout simplement pas que leur expression pour le C++ soit mature : http://herbsutter.wordpress.com/2009/07/21/trip-report/

Les types statiques tentent de vous empêcher de transmettre un objet à une fonction qui ne satisfait pas aux exigences de la fonction.En C++, c'est un gros problème, car au moment où le code accède à l'objet, il n'y a aucune vérification que c'est la bonne chose.

Les concepts tentent de vous empêcher de transmettre un paramètre de modèle qui ne satisfait pas aux exigences du modèle.Mais au moment où le compilateur accède au paramètre template, il y a déjà est vérifier que c'est la bonne chose, même sans Concepts.Si vous essayez de l'utiliser d'une manière qu'il ne prend pas en charge, vous obtenez une erreur du compilateur[*].Dans le cas d'un code utilisant beaucoup de modèles, vous pouvez obtenir trois écrans remplis de crochets angulaires, mais en principe, il s'agit d'un message informatif.La nécessité de détecter les erreurs avant un échec de compilation est moins urgente que la nécessité de détecter les erreurs avant un comportement indéfini au moment de l'exécution.

Les concepts facilitent la spécification des interfaces de modèles qui fonctionneront sur plusieurs instanciations.C'est un problème important, mais beaucoup moins urgent que la spécification d'interfaces de fonction qui fonctionneront sur plusieurs appels.

En réponse à votre question, toute déclaration formelle "J'implémente cette interface" présente un gros inconvénient, à savoir qu'elle nécessite que l'interface soit inventée avant que l'implémentation ne le soit.Ce n'est pas le cas des systèmes d'inférence de types, mais ils présentent le gros inconvénient que les langages en général ne peuvent pas exprimer l'intégralité d'une interface à l'aide de types. Vous pouvez donc avoir un objet dont on déduit qu'il est du type correct, mais qui n'a pas le type correct. sémantique attribuée à ce type.Si votre langage traite des interfaces (en particulier s'il les associe à des classes), alors AFAIK, vous devez prendre position ici et choisir votre désavantage.

[*] Généralement.Il existe quelques exceptions, par exemple, le système de types C++ ne vous empêche actuellement pas d'utiliser un itérateur d'entrée comme s'il s'agissait d'un itérateur de transfert.Vous avez besoin de traits d'itérateur pour cela.Taper un canard à lui seul ne vous empêche pas de dépasser un objet qui marche, nage et cancane, mais après une inspection minutieuse, il ne fait aucune de ces choses comme le fait un canard, et il est étonné d'apprendre que vous pensiez que cela le ferait ;-)

Autres conseils

Les interfaces n'ont pas besoin d'être basées sur l'héritage...c'est une décision de conception différente et distincte.Le nouveau Aller le langage a des interfaces, mais n'a pas d'héritage, par exemple :"un type satisfait automatiquement toute interface qui spécifie un sous-ensemble de ses méthodes", comme le dit Go FAQ le met.Simionato réflexions sur l'héritage et les interfaces, suscité par la récente version de Go, peut valoir la peine d'être lu.

Je suis d'accord que les classes de types sont encore plus puissantes, essentiellement parce que, comme classes de base abstraites, ils vous permettent en outre de spécifier du code utile (définissant une méthode supplémentaire X par rapport aux autres pour tous les types qui correspondent autrement à la classe de base mais ne définissent pas X eux-mêmes) - sans le bagage d'héritage que les ABC (contrairement aux interfaces) portent presque inévitablement . Presque inévitablement parce que, par exemple, les ABC de Python « font croire » qu'ils impliquent l'héritage, au niveau de la conceptualisation qu'ils proposent...mais, en fait, ils n'ont pas besoin d'être basés sur l'héritage (beaucoup vérifient simplement la présence et la signature de certaines méthodes, tout comme les interfaces de Go).

Quant à savoir pourquoi un concepteur de langage (comme Guido, dans le cas de Python) choisirait des « loups déguisés en mouton » comme l'ABC de Python, plutôt que les classes de types plus simples de type Haskell que j'avais proposées depuis 2002, c'est une question plus difficile. question à laquelle répondre.Après tout, ce n'est pas comme si Python avait quelque scrupule à emprunter des concepts à Haskell (par exemple, compréhensions de liste / expressions génératrices - Python a besoin d'une dualité ici, alors que Haskell n'en a pas, car Haskell est "paresseux").La meilleure hypothèse que je puisse proposer est que, à présent, l'héritage est si familier à la plupart des programmeurs que la plupart des concepteurs de langage estiment qu'ils peuvent être plus facilement acceptés en présentant les choses de cette façon (bien que les concepteurs de Go doivent être félicités de ne pas l'avoir fait).

Permettez-moi de commencer en gras :Je comprends parfaitement la motivation de l'avoir et je ne comprends pas la motivation de certaines personnes pour s'y opposer...

Ce que tu veux c'est polymorphisme ad hoc non virtuel.

  • ad hoc:la mise en œuvre peut varier
  • non virtuel :pour des raisons de performances ;répartition à la compilation

Le reste est du sucre à mon avis.

C++ dispose déjà d'un polymorphisme ad hoc via des modèles.Les "concepts" clarifieraient cependant quel type de fonctionnalité polymorphe ad hoc est utilisé par quelle entité définie par l'utilisateur.

C# n’a tout simplement aucun moyen de le faire. Une approche qui ne serait pas non virtuel:Si des types comme float implémentaient simplement quelque chose comme "INumeric" ou "IAddable" (...) nous serions au moins capables d'écrire un min, max, lerp générique et basé sur cette pince, maprange, bezier (...) .Mais ce ne serait pas rapide.Vous ne voulez pas ça.

Façons de résoudre ce problème :Puisque .NET effectue de toute façon la compilation JIT, il génère également un code différent pour List<int> que pour List<MyClass> (en raison des différences entre les types de valeur et de référence), cela n'ajouterait probablement pas beaucoup de surcharge pour générer également un code différent pour les parties polymorphes ad hoc.Le langage C# aurait juste besoin d'un moyen de l'exprimer. Sens Unique c'est ce que vous avez esquissé.

Une autre façon serait d'ajouter des contraintes de type à la fonction en utilisant une fonction polymorphe ad hoc :

    U SuperSquare<T, U>(T a) applying{ 
         nonvirtual operator (*) T (T, T) 
         nonvirtual Foo U (T)
    }
    {
        return Foo(a * a);
    }

Bien sûr, vous pourriez vous retrouver avec de plus en plus de contraintes lors de l'implémentation de Bar qui utilise Foo.Vous souhaiterez donc peut-être un mécanisme pour donner un nom à plusieurs contraintes que vous utilisez régulièrement...Cependant, c'est encore du sucre et une façon de l'aborder serait simplement d'utiliser le concept de classe de types...

Donner un nom à plusieurs contraintes, c'est comme définir une classe de types, mais j'aimerais simplement le considérer comme une sorte de mécanisme d'abréviation - du sucre pour une collection arbitraire de contraintes de type de fonction :

    // adhoc is like an interface: it is about collecting signatures
    // but it is not a type: it dissolves during compilation 
    adhoc AMyNeeds<T, U>
    {
         nonvirtual operator (*) T (T, T) 
         nonvirtual Foo U (T)
    } 

    U SuperSquare<T, U>(T a) applying AMyNeeds<T, U>        
    {
        return Foo(a * a);
    }

À un endroit "principal", tous les arguments de type sont connus et tout devient concret et peut être compilé ensemble.

Ce qui manque encore, c'est le manque de création d'implémentations différentes.Dans l'exemple supérieur, nous avons juste utilisé fonctions polymorphes et faites-le savoir à tout le monde...

Là encore, l'implémentation pourrait suivre la voie des méthodes d'extension - dans leur capacité à ajouter des fonctionnalités à n'importe quelle classe à tout moment :

 public static class SomeAdhocImplementations
 {
    public nonvirtual int Foo(float x)
    {
        return round(x);
    }
 }

En main, vous pouvez maintenant écrire :

    int a = SuperSquare(3.0f); // 3.0 * 3.0 = 9.0 rounded should return 9

Le compilateur vérifie toutes les fonctions ad hoc "non virtuelles", trouve à la fois un opérateur float (*) intégré et un int Foo (float) et est donc capable de compiler cette ligne.

Le polymorphisme ad hoc présente bien sûr l'inconvénient que vous devez recompiler pour chaque type de compilation afin que les bonnes implémentations soient insérées.Et probablement IL ne prend pas en charge la mise en place de cela dans une DLL.Mais peut-être qu'ils y travaillent quand même...

Je ne vois pas de réel besoin d’instanciation d’une construction de classe de types.Si quelque chose échoue lors de la compilation, nous obtenons les erreurs des contraintes ou si celles-ci étaient liées avec un codeclock "adhoc", le message d'erreur pourrait devenir encore plus lisible.

    MyColor a = SuperSquare(3.0f); 
    // error: There are no ad hoc implementations of AMyNeeds<float, MyColor> 
    // in particular there is no implementation for MyColor Foo(float)

Mais bien sûr, l'instanciation d'une classe de types / "interface de polymorphisme ad hoc" est également envisageable.Le message d'erreur indiquerait alors :"The AMyNeeds constraint of SuperSquare has not been matched. AMyNeeds is available as StandardNeeds : AMyNeeds<float, int> as defined in MyStandardLib".Il serait également possible de mettre l'implémentation dans une classe avec d'autres méthodes et d'ajouter "l'interface adhoc" à la liste des interfaces prises en charge.

Mais indépendamment de la conception particulière du langage :je ne vois pas l'inconvénient de les ajouter d'une manière ou d'une autre.Les langages à typage statique devront toujours repousser les limites du pouvoir d'expression, car ils ont commencé par en autoriser trop peu, ce qui tend à être un ensemble plus petit de pouvoir d'expression qu'un programmeur normal aurait pu s'attendre à ce qu'il soit possible...

à savoir :je suis de ton côté.Des trucs comme ça sont nuls dans les langages traditionnels à typage statique.Haskell a montré la voie.

Quel est le raisonnement contre les classes de types ?

La complexité de mise en œuvre pour les rédacteurs de compilateurs est toujours une préoccupation lorsqu'ils envisagent de nouvelles fonctionnalités de langage.C++ a déjà commis cette erreur et nous avons déjà souffert des années de compilateurs C++ bogués en conséquence.

Les interfaces sont basées sur l'héritage et donc relativement lentes en raison de l'indirection et de plus il n'y a aucune possibilité de laisser un type existant les implémenter

Pas vrai.Regardez le système d'objets structurellement typés d'OCaml, par exemple :

# let foo obj = obj#bar;;
val foo : < bar : 'a; .. > -> 'a = <fun>

Que foo la fonction accepte tout objet de tout type qui fournit les éléments nécessaires bar méthode.

Idem pour le système de modules d'ordre supérieur de ML.En fait, il existe même une équivalence formelle entre cela et les classes types.En pratique, les classes de types conviennent mieux aux abstractions à petite échelle telles que la surcharge d'opérateurs, tandis que les modules d'ordre supérieur conviennent mieux aux abstractions à grande échelle telles que le paramétrage par Okasaki des listes caténables sur les files d'attente.

Ont-ils finalement de sérieux inconvénients ?

Regardez votre propre exemple, l'arithmétique générique.F# peut déjà gérer ce cas particulier grâce au INumeric interface.Le F# Matrix type utilise même cette approche.

Cependant, vous venez de remplacer le code machine pour l'ajout par une répartition dynamique vers une fonction distincte, ce qui ralentit les ordres de grandeur arithmétiques.Pour la plupart des applications, c'est inutilement lent.Vous pouvez résoudre ce problème en effectuant des optimisations complètes du programme, mais cela présente des inconvénients évidents.De plus, il y a peu de points communs entre les méthodes numériques pour int contre float en raison de la robustesse numérique, votre abstraction est également pratiquement inutile.

La question devrait sûrement être :quelqu'un peut-il présenter un argument convaincant pour l'adoption de classes types ?

Mais aucun « langage grand public » ne fournit toujours de [classes de types.]

Lorsque cette question a été posée, cela aurait pu être vrai.Aujourd’hui, il existe un intérêt beaucoup plus fort pour des langues comme Haskell et Clojure.Haskell a des classes de types (class / instance), Clojure 1.2+ a protocoles (defprotocol / extend).

Quel est le raisonnement contre les [classes de types] ?

Je ne pense pas que les classes de types soient objectivement « pires » que les autres mécanismes de polymorphisme ;ils suivent simplement une approche différente.La vraie question est donc la suivante : s’intègrent-ils bien dans un langage de programmation particulier ?

Examinons brièvement en quoi les classes de types diffèrent des interfaces dans des langages tels que Java ou C#.Dans ces langages, une classe ne prend en charge que les interfaces explicitement mentionnées et implémentées dans la définition de cette classe.Les classes de types, cependant, sont des interfaces qui peuvent être ultérieurement ajoutées à n'importe quel type déjà défini, même dans un autre module.Ce type d'extensibilité de type est évidemment assez différent des mécanismes de certains langages OO « grand public ».


Considérons maintenant les classes de types pour quelques langages de programmation courants.

Haskell:Inutile de dire que cette langue a cours de type.

Clojure:Comme indiqué ci-dessus, Clojure a quelque chose comme des classes de types sous la forme de protocoles.

C++:Comme vous l'avez dit vous-même, notions ont été supprimés de la spécification C++11.

Au contraire:Les concepts, qui sont une idée assez analogique, ont été exclus du prochain C++ !

Je n'ai pas suivi tout le débat autour de cette décision.D'après ce que j'ai lu, les concepts n'étaient pas "encore prêts" :Il y avait encore un débat sur les cartes conceptuelles.Cependant, les concepts n'ont pas été complètement abandonnés, ils devraient figurer dans la prochaine version de C++.

C#:Avec la version 3 du langage, C# est essentiellement devenu un hybride des paradigmes de programmation orientée objet et fonctionnel.Un ajout a été apporté au langage qui est conceptuellement assez similaire aux classes de types : méthodes d'extension.La principale différence est que vous attachez (apparemment) de nouvelles méthodes à un type existant, pas à des interfaces.

(Certes, le mécanisme de la méthode d'extension n'est pas aussi élégant que celui de Haskell. instance … where syntaxe.Les méthodes d'extension ne sont pas "vraiment" attachées à un type, elles sont implémentées comme une transformation syntaxique.Mais en fin de compte, cela ne fait pas une très grande différence pratique.)

Je ne pense pas que cela se produira de si tôt : les concepteurs du langage n'ajouteront probablement même pas d'extension. propriétés à la langue et à l'extension interfaces irait même plus loin que cela.

(VB.NET:Microsoft "co-évolue" les langages C# et VB.NET depuis un certain temps, donc mes déclarations sur C# sont également valables pour VB.NET.)

Java:Je ne connais pas très bien Java, mais parmi les langages C++, C# et Java, c'est probablement le langage OO "le plus pur".Je ne vois pas comment les classes de types s'intégreraient naturellement dans ce langage.

F#:J'ai trouvé un message sur le forum expliquant pourquoi les classes de types pourraient ne jamais être introduites dans F#.Cette explication est centrée sur le fait que F# a un système de type nominatif et non structurel.(Bien que je ne sois pas certain que ce soit une raison suffisante pour que F# n'ait pas de classes de types.)

Essayez de définir un Matroid, ce que nous faisons (logiquement et non oralement en disant un Matroid), et c'est toujours probablement quelque chose comme une structure C.Principe de Liskov (dernier médaillé de Turing) devient trop abstrait, trop catégorique, trop théorique, traitant moins de données réelles et un système de classes théorique plus pur, pour une résolution pratique et pragmatique de problèmes, je l'ai brièvement jeté un coup d'œil qui ressemblait à PROLOG, code sur code à propos de code à propos de code... tandis qu'un algorithme décrit des séquences et des voyages que nous comprenons sur papier ou au tableau.Cela dépend de votre objectif, résoudre un problème avec un minimum de code ou le plus abstrait.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top