Quelle est la différence entre les traits dans Rust et les classes de types dans Haskell?

157

Les traits de Rust semblent au moins superficiellement similaires aux classes de types de Haskell, mais j'ai vu des gens écrire qu'il y avait des différences entre eux. Je me demandais exactement quelles étaient ces différences.

LogicChains
la source
8
Je ne sais pas grand-chose sur Rust. Mais les pierres d'achoppement courantes pour des technologies similaires dans d'autres langages sont des types plus élevés (par exemple, les traits peuvent-ils aller sur des types paramétrés, mais pas leurs paramètres?) Et le polymorphisme de type retour (par exemple, un type de trait peut-il apparaître dans le résultat d'une fonction, mais pas n'importe où dans les arguments?). Un exemple du premier à Haskell est class Functor f where fmap :: (a -> b) -> (f a -> f b); un exemple de ce dernier est class Bounded a where maxBound :: a.
Daniel Wagner
4
GHC prend également en charge les classes de types multi-paramètres (c'est-à-dire les traits impliquant plusieurs types) et les dépendances fonctionnelles, bien que cela ne fasse pas partie de la spécification officielle Haskell. À en juger par la syntaxe Rust suggérée sur votre lien, il ne peut prendre en charge que des traits allant de plus d'un type à la fois, bien que ce jugement ne soit pas encore basé sur une expérience approfondie.
Daniel Wagner
4
@DanielWagner Le polymorphisme de type retour existe (par exemple std::default), et les traits multiparamètres fonctionnent en quelque sorte (y compris un analogue des dépendances fonctionnelles), même si AFAIK il faut contourner le premier paramètre privilégié. Pas de HKT cependant. Ils sont sur la liste de souhaits pour un futur lointain mais pas encore à l'horizon.
4
une autre différence est le traitement des instances orphelines. Rust essaie d'avoir des règles de cohérence plus strictes sur l'endroit où un nouvel impl pour un trait peut être écrit. Voir cette discussion pour plus de détails (en particulier ici )
Paolo Falabella
1
Rust prend désormais en charge les types associés et les contraintes d'égalité , bien qu'ils ne soient pas aussi puissants que les familles de types de Haskell. Il a également des types existentiels via des objets de trait .
Lambda Fairy

Réponses:

61

Au niveau de base, il n'y a pas beaucoup de différence, mais ils sont toujours là.

Haskell décrit les fonctions ou les valeurs définies dans une classe de types comme des «méthodes», tout comme les traits décrivent les méthodes POO dans les objets qu'ils renferment. Cependant, Haskell les traite différemment, les traitant comme des valeurs individuelles plutôt que de les épingler à un objet comme la POO le conduirait à le faire. Il s'agit de la différence la plus évidente au niveau de la surface.

La seule chose que Rust ne pouvait pas faire pendant un certain temps était les traits typés d'ordre supérieur , tels que les fameuses classes Functoret les Monadclasses de types.

Cela signifie que les traits Rust ne pouvaient décrire que ce qu'on appelle souvent un «type concret», en d'autres termes, un sans argument générique. Haskell pouvait dès le départ créer des classes de types d'ordre supérieur qui utilisent des types similaires à la façon dont les fonctions d'ordre supérieur utilisent d'autres fonctions: en utilisant l'une pour en décrire une autre. Pendant un certain temps, cela n'a pas été possible dans Rust, mais depuis que les éléments associés ont été mis en œuvre, ces caractéristiques sont devenues courantes et idiomatiques.

Donc, si nous ignorons les extensions, elles ne sont pas exactement les mêmes, mais chacune peut se rapprocher de ce que l'autre peut faire.

Il est également mentionné, comme indiqué dans les commentaires, que GHC (le compilateur principal de Haskell) prend en charge d'autres options pour les classes de types, y compris les classes de types à paramètres multiples (c'est -à- dire de nombreux types impliqués) et les dépendances fonctionnelles , une belle option qui permet des calculs au niveau du type et mène aux familles de types . À ma connaissance, Rust n'a ni funDeps ni familles de types, bien que cela puisse se produire à l'avenir. †

Dans l'ensemble, les traits et les classes de types ont des différences fondamentales qui, en raison de la façon dont ils interagissent, les font agir et semblent assez similaires à la fin.


† Un bel article sur les classes de types de Haskell (y compris les classes les plus typées) peut être trouvé ici , et le chapitre Rust by Example sur les traits peut être trouvé ici

AJFarmar
la source
1
Rust n'a toujours aucune forme de types de types supérieurs. "Infamous" a besoin de justification. Les Functor sont incroyablement répandus et utiles en tant que concept. Les familles de types sont les mêmes que les types associés. Les dépendances fonctionnelles sont essentiellement redondantes avec les types associés (y compris dans Haskell). La chose qui manque à Rust. fundeps est des annotations d'injectivité. Vous l'avez à l'envers, les traits de Rust et les classes de types de Haskell sont différents en surface mais de nombreuses différences s'évaporent lorsque vous regardez en dessous. Les différences qui subsistent sont essentiellement inhérentes aux différents domaines dans
lesquels
Les éléments associés sont désormais considérés comme idomatiques dans de nombreuses circonstances, non?
Vaelus
@Vaelus Vous avez raison - Cette réponse devrait être mise à jour un peu. Édition maintenant.
AJFarmar
19

Je pense que les réponses actuelles négligent les différences les plus fondamentales entre les traits Rust et les classes de type Haskell. Ces différences sont liées à la manière dont les traits sont liés aux constructions de langage orientées objet. Pour plus d'informations à ce sujet, consultez le livre Rust .

  1. Une déclaration de trait crée un type de trait . Cela signifie que vous pouvez déclarer des variables d'un tel type (ou plutôt des références du type). Vous pouvez également utiliser des types de trait comme paramètres sur des fonctions, des champs de structure et des instanciations de paramètres de type.

    Une variable de référence de trait peut, au moment de l'exécution, contenir des objets de types différents, tant que le type d'exécution de l'objet référencé implémente le trait.

    // The shape variable might contain a Square or a Circle, 
    // we don't know until runtime
    let shape: &Shape = get_unknown_shape();
    
    // Might contain different kinds of shapes at the same time
    let shapes: Vec<&Shape> = get_shapes();

    Ce n'est pas ainsi que fonctionnent les classes de types. Les classes de type ne créent aucun type , vous ne pouvez donc pas déclarer de variables avec le nom de classe. Les classes de types agissent comme des limites sur les paramètres de type, mais les paramètres de type doivent être instanciés avec un type concret, pas la classe de type elle-même.

    Vous ne pouvez pas avoir une liste de différents éléments de types différents qui implémentent la même classe de type. (Au lieu de cela, les types existentiels sont utilisés dans Haskell pour exprimer une chose similaire.) Note 1

  2. Les méthodes de trait peuvent être distribuées dynamiquement . Ceci est fortement lié aux choses qui sont décrites dans la section ci-dessus.

    La répartition dynamique signifie que le type d'exécution de l'objet pointé par une référence est utilisé pour déterminer la méthode appelée via la référence.

    let shape: &Shape = get_unknown_shape();
    
    // This calls a method, which might be Square.area or
    // Circle.area depending on the runtime type of shape
    print!("Area: {}", shape.area());

    Encore une fois, des types existentiels sont utilisés pour cela dans Haskell.

En conclusion

Il me semble que les traits sont à bien des égards le même concept que les classes de types. De plus, ils ont la fonctionnalité d'interfaces orientées objet.

En revanche, les classes de types de Haskell sont plus avancées. Haskell a par exemple des types plus élevés et des extensions comme des classes de types multi-paramètres.


Remarque 1 : Les versions récentes de Rust ont une mise à jour pour différencier l'utilisation des noms de trait comme types et l'utilisation des noms de trait comme limites. Dans un type de trait, le nom est précédé du dynmot - clé. Voir par exemple cette réponse pour plus d'informations.

Lii
la source
2
"Les classes de types ne créent aucun type" - Je pense qu'il est préférable de les comprendre dyn Traitcomme une forme de typage existentiel en ce qui concerne les traits / classes de types. On peut considérer dynun opérateur sur les bornes les projetant vers des types, ie dyn : List Bound -> Type. En prenant cette idée de Haskell, et en ce qui concerne «vous ne pouvez pas déclarer des variables avec le nom de classe », nous pouvons le faire indirectement en Haskell: data Dyn (c :: * -> Constraint) = forall (t :: Type). c t => D t. Après avoir défini cela, nous pouvons travailler avec [D True, D "abc", D 42] :: [D Show].
Centril
8

Les «traits» de Rust sont analogues aux classes de types de Haskell.

La principale différence avec Haskell est que les traits n'interviennent que pour les expressions avec notation par points, c'est-à-dire de la forme a.foo (b).

Les classes de type Haskell s'étendent aux types d'ordre supérieur. Les traits Rust ne prennent en charge que les types d'ordre supérieur car ils sont absents de l'ensemble du langage, c'est-à-dire que ce n'est pas une différence philosophique entre les traits et les classes de types

Anuj Gupta
la source
1
Les traits de Rust n'interviennent pas "uniquement pour les expressions avec notation par points". Par exemple, considérons le Defaulttrait qui n'a pas de méthodes, uniquement des fonctions non associées à des méthodes.
Centril