Rust: Problème de suivi pour « impl Trait » (RFC 1522, RFC 1951, RFC 2071)

Créé le 27 juin 2016  ·  417Commentaires  ·  Source: rust-lang/rust

NOUVEAU PROBLÈME DE SUIVI = https://github.com/rust-lang/rust/issues/63066

État de mise en œuvre

La fonctionnalité de base telle que spécifiée dans la RFC 1522 est implémentée, cependant il y a eu des révisions qui ont encore besoin de travail :

RFC

Il y a eu un certain nombre de RFC concernant le trait impl, qui sont toutes suivies par ce problème de suivi central.

Questions non résolues

La mise en œuvre a également soulevé un certain nombre de questions intéressantes :

  • [x] Quelle est la priorité du mot-clé impl lors de l'analyse des types ? Débat : 1
  • [ ] Devrions-nous autoriser impl Trait après -> dans les types fn ou le sucre entre parenthèses ? #45994
  • [ ] Devons-nous imposer un DAG à toutes les fonctions pour permettre une fuite auto-sécurisée, ou pouvons-nous utiliser une sorte de report. Débat : 1

    • Sémantique présente : DAG.

  • [x] Comment intégrer impl trait dans regionck ? Débat : 1 , 2
  • [ ] Doit-on autoriser la spécification de types si certains paramètres sont implicites et d'autres explicites ? par exemple, fn foo<T>(x: impl Iterator<Item = T>>) ?
  • [ ] [Quelques préoccupations concernant l'utilisation des traits impl imbriqués] (https://github.com/rust-lang/rust/issues/34511#issuecomment-350715858)
  • [x] La syntaxe dans un impl devrait-elle être existential type Foo: Bar ou type Foo = impl Bar ? ( voir ici pour la discussion )
  • [ ] L'ensemble des « utilisations de définition » pour un existential type dans un impl doit-il être uniquement des éléments de l'impl, ou inclure des éléments imbriqués dans les fonctions impl, etc ? ( voir ici par exemple )
B-RFC-implemented B-unstable C-tracking-issue T-lang disposition-merge finished-final-comment-period

Commentaire le plus utile

Comme il s'agit de la dernière chance avant la fermeture de FCP, j'aimerais faire un dernier argument contre les traits automatiques automatiques. Je me rends compte que c'est un peu à la dernière minute, donc j'aimerais tout au plus aborder formellement ce problème avant de nous engager dans la mise en œuvre actuelle.

Pour clarifier pour tous ceux qui n'ont pas suivi impl Trait , c'est le problème que je présente. Un type représenté par des types impl X implémente actuellement automatiquement les traits automatiques si et seulement si le type concret derrière eux implémente lesdits traits automatiques. Concrètement, si le changement de code suivant est effectué, la fonction continuera à se compiler, mais toute utilisation de la fonction reposant sur le fait que le type qu'elle renvoie implémente Send échouera.

 fn does_some_operation() -> impl Future<Item=(), Error=()> {
-    let data_stored = Arc::new("hello");
+    let data_stored = Rc::new("hello");

     return some_long_operation.and_then(|other_stuff| {
         do_other_calculation_with(data_stored)
     });
}

(exemple plus simple : travailler , les changements internes provoquent un échec )

Cette question n'est pas tranchée. Il y avait une décision très délibérée d'avoir des "fuites" de traits automatiques : si nous ne le faisions pas, nous devions mettre + !Send + !Sync sur chaque fonction qui renvoie quelque chose de non-Send ou non-Sync, et nous avoir une histoire peu claire avec d'autres traits automatiques personnalisés potentiels qui pourraient tout simplement ne pas être implémentables sur le type concret que la fonction renvoie. Ce sont deux problèmes que j'aborderai plus tard.

Tout d'abord, je voudrais simplement exprimer mon objection au problème : cela permet de modifier le corps d'une fonction pour changer l'API publique. Cela réduit directement la maintenabilité du code.

Tout au long du développement de la rouille, des décisions ont été prises qui privilégient la verbosité plutôt que la convivialité. Lorsque les nouveaux arrivants les voient, ils pensent que c'est de la verbosité pour la verbosité, mais ce n'est pas le cas. Chaque décision, qu'il s'agisse de ne pas avoir de structures implémentant automatiquement la copie, ou d'avoir tous les types explicites au niveau des signatures de fonction, est prise dans un souci de maintenabilité.

Lorsque je présente Rust aux gens, bien sûr, je peux leur montrer la vitesse, la productivité, la sécurité de la mémoire. Mais aller a de la vitesse. Ada a la sécurité de la mémoire. Python a de la productivité. Ce que Rust a l'emporte sur tout cela, il a la maintenabilité. Lorsqu'un auteur de bibliothèque souhaite modifier un algorithme pour qu'il soit plus efficace, ou lorsqu'il souhaite refaire la structure d'un crate, il a une forte garantie de la part du compilateur qu'il le dira en cas d'erreur. En rouille, je peux être assuré que mon code continuera à fonctionner non seulement en termes de sécurité de la mémoire, mais aussi de logique et d'interface. _Chaque interface de fonction dans Rust est entièrement représentable par la déclaration de type de la fonction_.

Stabiliser impl Trait tel quel a de grandes chances d'aller à l'encontre de cette croyance. Bien sûr, c'est extrêmement agréable pour écrire du code rapidement, mais si je veux prototyper, j'utiliserai python. Rust est le langage de choix lorsqu'on a besoin d'une maintenabilité à long terme, et non d'un code en écriture seule à court terme.


Je dis qu'il n'y a qu'une "grande chance" que cela soit mauvais ici, car encore une fois, le problème n'est pas clair. L'idée de « traits automatiques » en premier lieu n'est pas explicite. Send et Sync sont implémentés en fonction du contenu d'une structure, et non de la déclaration publique. Étant donné que cette décision a fonctionné pour la rouille, impl Trait agissant de la même manière pourrait également bien fonctionner.

Cependant, les fonctions et les structures sont utilisées différemment dans une base de code, et ce ne sont pas les mêmes problèmes.

Lorsqu'on modifie les champs d'une structure, même des champs privés, on comprend immédiatement qu'on en change le contenu réel. Les structures avec des champs non-Send ou non-Sync ont fait ce choix, et les responsables de la bibliothèque savent qu'ils doivent vérifier lorsqu'un PR modifie les champs d'une structure.

Lors de la modification des éléments internes d'une fonction, il est clair que cela peut affecter à la fois les performances et l'exactitude. Cependant, dans Rust, nous n'avons pas besoin de vérifier que nous renvoyons le bon type. Les déclarations de fonction sont un contrat difficile que nous devons respecter, et rustc veille sur nos arrières. C'est une ligne mince entre les traits automatiques sur les structures et dans les retours de fonction, mais changer les éléments internes d'une fonction est beaucoup plus routinier. Une fois que nous aurons des Future entièrement alimentés par un générateur, il sera encore plus courant de modifier les fonctions renvoyant -> impl Future . Ce seront tous des changements que les auteurs doivent rechercher pour les implémentations Send/Sync modifiées si le compilateur ne les détecte pas.

Pour résoudre ce problème, nous pourrions décider qu'il s'agit d'une charge de maintenance acceptable, comme l'a fait la discussion RFC originale . Cette section de la RFC sur le

J'ai déjà exposé ma réponse principale à cela, mais voici une dernière note. Changer la disposition d'une structure n'est pas si courant ; on peut s'en prémunir. La charge de maintenance pour s'assurer que les fonctions continuent à implémenter les mêmes traits automatiques est plus importante que celle des structures simplement parce que les fonctions changent beaucoup plus.


Pour terminer, je voudrais dire que les traits automatiques automatiques ne sont pas la seule option. C'est l'option que nous avons choisie, mais l'alternative des traits automatiques opt-out est toujours une alternative.

Nous pourrions exiger que les fonctions renvoyant des éléments non-Send / non-Sync soient à l'état + !Send + !Sync ou renvoient un trait (alias éventuellement ?) qui a ces limites. Ce ne serait pas une bonne décision, mais elle pourrait être meilleure que celle que nous choisissons actuellement.

En ce qui concerne la préoccupation concernant les traits automatiques personnalisés, je dirais que tout nouveau trait automatique ne devrait pas être mis en œuvre uniquement pour les nouveaux types introduits après le trait automatique. Cela pourrait poser plus de problèmes que je ne peux en traiter maintenant, mais ce n'est pas un problème que nous ne pouvons pas résoudre avec plus de conception.


C'est très tard et très long, et je suis certain d'avoir déjà soulevé ces objections. Je suis content de pouvoir commenter une dernière fois et de m'assurer que nous sommes entièrement d'accord avec la décision que nous prenons.

Merci d'avoir lu, et j'espère que la décision finale mettra Rust dans la meilleure direction possible.

Tous les 417 commentaires

@aturon Pouvons-nous réellement mettre le RFC dans le référentiel ? ( @mbrubeck y a commenté que c'était un problème.)

Fait.

La première tentative d'implémentation est #35091 (deuxièmement, si vous comptez ma branche de l'année dernière).

Un problème que j'ai rencontré est celui des vies. L'inférence de type aime mettre des variables de région _partout_ et sans aucune modification de vérification de région, ces variables n'infèrent rien d'autre que des portées locales.
Cependant, le type concret _doit_ être exportable, je l'ai donc restreint à 'static et nommé explicitement les paramètres de durée de vie liés au début, mais ce n'est _jamais_ aucun de ceux-ci si une fonction est impliquée - même un littéral de chaîne n'infère pas à 'static , c'est à peu près complètement inutile.

Une chose à laquelle j'ai pensé, qui n'aurait aucun impact sur la vérification de région elle-même, est d'effacer les durées de vie :

  • rien exposant le type concret d'un impl Trait ne devrait se soucier des durées de vie - une recherche rapide de Reveal::All suggère que c'est déjà le cas dans le compilateur
  • une limite doit être placée sur tous les types concrets de impl Trait dans le type de retour d'une fonction, qu'elle survit à l'appel de cette fonction - cela signifie que toute durée de vie est, par nécessité, soit 'static ou l'un des paramètres de durée de vie de la fonction - _even_ si nous ne pouvons pas savoir lequel (par exemple "le plus court de 'a et 'b ")
  • nous devons choisir une variance pour le paramètre de durée de vie implicite de impl Trait (c. plus et nécessiterait de vérifier que chaque durée de vie dans le type de retour est dans une position contravariante (même chose avec le paramètre de type covariant au lieu d'invariant)
  • le mécanisme de fuite de trait automatique nécessite qu'un trait lié puisse être mis sur le type concret, dans une autre fonction - puisque nous avons effacé les durées de vie et n'avons aucune idée de quelle durée de vie va où, chaque durée de vie effacée dans le type concret devra être remplacée avec une nouvelle variable d'inférence qui est garantie de ne pas être plus courte que la durée de vie la plus courte parmi tous les paramètres de durée de vie réels ; le problème réside dans le fait que les traits impls peuvent finir par nécessiter des relations plus solides à vie (par exemple X<'a, 'a> ou X<'static> ), qui doivent être détectées et erronées, car elles ne peuvent pas être prouvées pour ces vies

Ce dernier point sur la fuite des traits automatiques est mon seul souci, tout le reste semble simple.
Il n'est pas tout à fait clair à ce stade de la quantité de vérification de région que nous pouvons réutiliser telle quelle. Espérons que tous.

cc @rust-lang/lang

@eddyb

Mais les vies _sont_ importantes avec impl Trait - par exemple

fn get_debug_str(s: &str) -> impl fmt::Debug {
    s
}

fn get_debug_string(s: &str) -> impl fmt::Debug {
    s.to_string()
}

fn good(s: &str) -> Box<fmt::Debug+'static> {
    // if this does not compile, that would be quite annoying
    Box::new(get_debug_string())
}

fn bad(s: &str) -> Box<fmt::Debug+'static> {
    // if this *does* compile, we have a problem
    Box::new(get_debug_str())
}

J'ai mentionné cela plusieurs fois dans les discussions RFC

version sans trait-objet :

fn as_debug(s: &str) -> impl fmt::Debug;

fn example() {
    let mut s = String::new("hello");
    let debug = as_debug(&s);
    s.truncate(0);
    println!("{:?}", debug);
}

C'est soit UB ou non selon la définition de as_debug .

@ arielb1 Ah, c'est vrai, j'ai oublié que l'une des raisons pour lesquelles j'ai fait ce que j'ai fait était de ne capturer que les paramètres de durée de vie, pas ceux anonymes liés tardivement, sauf que cela ne fonctionne pas vraiment.

@arielb1 Avons-nous une relation de survie stricte que nous pouvons mettre entre les durées de vie trouvées dans le type concret avant l'effacement et les durées de vie liées tardivement dans la signature ? Sinon, ce n'est peut-être pas une mauvaise idée de simplement regarder les relations à vie et d'insta-failer n'importe quel 'a outlives 'b direct ou _indirect_ où 'a est _n'importe quoi_ autre que 'static ou un paramètre de durée de vie et 'b apparaît dans le type concret d'un impl Trait .

Désolé d'avoir mis du temps à réécrire ici. Alors j'ai réfléchi
à propos de ce problème. Mon sentiment est que nous devons, en fin de compte, (et
voulez) étendre regionck avec un nouveau type de contrainte -- je l'appellerai
une contrainte \in , car elle vous permet de dire quelque chose comme '0 \in {'a, 'b, 'c} , ce qui signifie que la région utilisée pour '0 doit être
soit 'a , 'b , soit 'c . Je ne suis pas sûr de la meilleure façon d'intégrer
ceci en se résolvant -- certainement si l'ensemble \in est un singleton
ensemble, c'est juste une relation d'égalité (que nous n'avons pas actuellement comme
chose de première classe, mais qui peut être composée hors de deux limites), mais
sinon ça complique les choses.

Tout cela est lié à mon désir de rendre l'ensemble des contraintes régionales
plus expressif que ce que nous avons aujourd'hui. Certes, on pourrait composer un
Contrainte \in parmi les contraintes OR et == . Mais bien sûr plus
les contraintes expressives sont plus difficiles à résoudre et \in n'est pas différent.

Quoi qu'il en soit, permettez-moi d'exposer un peu ma réflexion ici. Travaillons avec ça
Exemple:

pub fn foo<'a,'b>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> {...}

Je pense que le désucrage le plus précis pour un impl Trait est probablement un
nouveau type:

pub struct FooReturn<'a, 'b> {
    field: XXX // for some suitable type XXX
}

impl<'a,'b> Iterator for FooReturn<'a,'b> {
    type Item = <XXX as Iterator>::Item;
}

Maintenant, le impl Iterator<Item=u32> dans foo devrait se comporter de la même manière que
FooReturn<'a,'b> se comporterait. Ce n'est pas un match parfait cependant. Un
la différence, par exemple, est la variance, comme eddyb l'a dit -- je suis
en supposant que nous allons rendre les types impl Foo -like invariants sur le type
paramètres de foo . Le comportement de trait automatique fonctionne, cependant.
(Un autre domaine où le match pourrait ne pas être idéal est si jamais nous ajoutons le
possibilité de "percer" l'abstraction impl Iterator , de sorte que le code
"à l'intérieur" l'abstraction connaît le type précis -- alors elle trierait
d'avoir une opération de "déballage" implicite.)

À certains égards, une meilleure correspondance consiste à considérer une sorte de trait synthétique :

trait FooReturn<'a,'b> {
    type Type: Iterator<Item=u32>;
}

impl<'a,'b> FooReturn<'a,'b> for () {
    type Type = XXX;
}

Maintenant, nous pouvons considérer le type impl Iterator comme <() as FooReturn<'a,'b>>::Type . Ce n'est pas non plus une correspondance parfaite, car nous
le normaliserait normalement. Vous pouvez imaginer utiliser la spécialisation
pour éviter ça :

trait FooReturn<'a,'b> {
    type Type: Iterator<Item=u32>;
}

impl<'a,'b> FooReturn<'a,'b> for () {
    default type Type = XXX; // can't really be specialized, but wev
}

Dans ce cas, <() as FooReturn<'a,'b>>::Type ne se normaliserait pas,
et nous avons un match beaucoup plus serré. La variance, en particulier, se comporte
à droite; si jamais nous voulions avoir un type qui soit "à l'intérieur" du
l'abstraction, ils seraient les mêmes mais ils sont autorisés à
normaliser. Cependant, il y a un hic : les trucs de trait automatique ne
assez de travail. (Nous pouvons envisager d'harmoniser les choses ici,
réellement.)

Quoi qu'il en soit, mon propos en explorant ces désucrages potentiels n'est pas de
suggérons que nous implémentions "impl Trait" comme un désucrage _réel_
(même si ça peut être sympa...) mais pour donner une intuition pour notre métier. je
pense que le deuxième désucrage -- en termes de projections -- est un
assez utile pour nous guider vers l'avant.

Un endroit où cette projection de désucrage est un guide vraiment utile est
la relation "survie". Si nous voulions vérifier si <() as FooReturn<'a,'b>>::Type: 'x , la RFC 1214 nous dit que nous pouvons le prouver
tant que 'a: 'x _and_ 'b: 'x tient. C'est je pense comment nous voulons
gérer les choses pour le trait impl également.

A l'heure du trans, et pour les auto-traits, il faudra savoir ce que XXX
est, bien sûr. L'idée de base ici, je suppose, est de créer un type
variable pour XXX et vérifiez que les valeurs réelles qui sont renvoyées
peuvent tous être unifiés avec XXX . Cette variable de type devrait, en théorie,
dites-nous notre réponse. Mais bien sûr le problème est que ce type
La variable peut faire référence à de nombreuses régions qui ne sont pas comprises dans le
signature fn -- par exemple, les régions du corps fn. (Ce même problème
ne se produit pas avec les types ; même si, techniquement, vous pourriez mettre
par exemple une déclaration de structure dans le corps de fn et ce serait innommable,
c'est une sorte de restriction artificielle -- on pourrait tout aussi bien se déplacer
la structure en dehors de la fn.)

Si vous regardez à la fois la struct desugaring ou l'impl, il y a un
(implicite dans la structure lexicale de Rust) restriction que XXX peut
nommez seulement 'static ou des vies comme 'a et 'b , qui
apparaissent dans la signature de la fonction. C'est la chose que nous ne sommes pas
modélisation ici. Je ne suis pas sûr de la meilleure façon de le faire - un certain type
les schémas d'inférence ont une représentation plus directe de la portée, et
J'ai toujours voulu ajouter cela à Rust, pour nous aider avec les fermetures. Mais
Pensons d'abord aux deltas plus petits, je suppose.

C'est de là que vient la contrainte \in . On peut imaginer ajouter
une règle de vérification de type qui (essentiellement) FR(XXX) \subset {'a, 'b} --
ce qui signifie que les "régions libres" apparaissant dans XXX ne peuvent être que 'a et
'b . Cela se traduirait par des exigences de \in pour le
diverses régions qui apparaissent dans XXX .

Regardons un exemple réel :

fn foo<'a,'b>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> {
    if condition { x.iter().cloned() } else { y.iter().cloned() }
}

Ici, le type si condition est vrai serait quelque chose comme
Cloned<SliceIter<'a, i32>> . Mais si condition est faux, nous
veux Cloned<SliceIter<'b, i32>> . Bien sûr, dans les deux cas, nous
finir avec quelque chose comme (en utilisant des nombres pour les variables de type/région) :

Cloned<SliceIter<'0, i32>> <: 0
'a: '0 // because the source is x.iter()
Cloned<SliceIter<'1, i32>> <: 0
'b: '1 // because the source is y.iter()

Si on instancie ensuite la variable 0 à Cloned<SliceIter<'2, i32>> ,
nous avons '0: '2 et '1: '2 , ou un ensemble total de relations régionales
Comme:

'a: '0
'0: '2
'b: '1
'1: '2
'2: 'body // the lifetime of the fn body

Alors quelle valeur devrions-nous utiliser pour '2 ? Nous avons également le supplément
contrainte que '2 in {'a, 'b} . Avec le fn tel qu'il est écrit, je pense que nous
devrait signaler une erreur, puisque ni 'a ni 'b n'est un
bon choix. Fait intéressant, cependant, si nous ajoutions la contrainte 'a: 'b , alors il y aurait une valeur correcte ( 'b ).

Notez que si nous exécutons simplement l'algorithme _normal_, nous nous retrouverons avec
'2 étant 'body . Je ne sais pas comment gérer les relations \in
sauf pour une recherche exhaustive (bien que je puisse imaginer des
cas).

OK, c'est tout ce que j'ai obtenu. =)

Sur le PR #35091, @arielb1 a écrit :

Je n'aime pas l'approche "capturer toutes les vies dans le trait impl" et préférerais quelque chose comme l'élision à vie.

J'ai pensé qu'il serait plus logique d'en discuter ici. @arielb1 , pouvez-vous nous en dire plus sur ce que vous avez en tête ? En termes d'analogies que j'ai faites ci-dessus, je suppose que vous parlez fondamentalement d'"élagage" de l'ensemble des durées de vie qui apparaîtraient soit en tant que paramètres sur le nouveau type, soit dans la projection (c'est-à-dire <() as FooReturn<'a>>::Type au lieu de <() as FooReturn<'a,'b>>::Type ou quelque chose ?

Je ne pense pas que les règles d'élision de durée de vie telles qu'elles existent seraient un bon guide à cet égard : si nous choisissions simplement la durée de vie de &self à inclure uniquement, nous ne serions pas nécessairement en mesure d'inclure le tapez les paramètres de la structure Self , ni les paramètres de type de la méthode, car ils peuvent avoir des conditions WF qui nous obligent à nommer certaines des autres durées de vie.

Quoi qu'il en soit, ce serait formidable de voir quelques exemples illustrant les règles que vous avez en tête, et peut-être leurs avantages. :) (En outre, je suppose que nous aurions besoin d'une syntaxe pour remplacer le choix.) Toutes choses étant égales par ailleurs, si nous pouvons éviter d'avoir à choisir parmi N vies, je préférerais cela.

Je n'ai vu aucune interaction de impl Trait avec la confidentialité discutée nulle part.
Désormais, fn f() -> impl Trait peut renvoyer un type privé S: Trait même manière que les objets trait fn f() -> Box<Trait> . C'est-à-dire que les objets de types privés peuvent circuler librement en dehors de leur module sous forme anonymisée.
Cela semble raisonnable et souhaitable - le type lui-même est un détail d'implémentation, seule son interface, disponible via un trait public Trait est publique.
Cependant, il y a une différence entre les objets trait et impl Trait . Avec les objets trait seuls, toutes les méthodes trait de types privés peuvent obtenir une liaison interne, elles seront toujours appelables via des pointeurs de fonction. Avec impl Trait s trait, les méthodes de types privés sont directement appelables à partir d'autres unités de traduction. L'algorithme faisant "l'internalisation" des symboles devra s'efforcer d'internaliser les méthodes uniquement pour les types non anonymisés avec impl Trait , ou d'être très pessimiste.

@nikomatsakis

La façon "explicite" d'écrire foo serait

fn foo<'a: 'c,'b: 'c,'c>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> + 'c {
    if condition { x.iter().cloned() } else { y.iter().cloned() }
}

Ici, il n'y a pas de question sur la durée de vie limitée. De toute évidence, avoir à écrire la durée de vie liée à chaque fois serait assez répétitif. Cependant, la façon dont nous traitons ce genre de répétition passe généralement par l'élision à vie. Dans le cas de foo , l'élision échouerait et forcerait le programmeur à spécifier explicitement les durées de vie.

Je suis opposé à l'ajout d'élision à vie sensible à l'explicitation comme @eddyb l'a fait uniquement dans le cas spécifique de impl Trait et pas autrement.

@arielb1 hmm, je ne sais pas à 100% comment penser à cette syntaxe proposée en termes de "désucrages" dont j'ai discuté. Il vous permet de spécifier ce qui semble être lié à une durée de vie, mais ce que nous essayons de déduire est principalement ce que les durées de vie apparaissent dans le type caché. Cela suggère-t-il qu'au plus une durée de vie pourrait être « cachée » (et qu'elle devrait être spécifiée exactement ?)

Il semble que ce ne soit pas toujours le cas qu'un "paramètre de durée de vie unique" suffise :

fn foo<'a, 'b>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> {
    x.iter().chain(y).cloned()
}

Dans ce cas, le type d'itérateur caché fait référence à la fois à 'a et à 'b (bien qu'il s'agisse d'une variante dans les deux ; mais je suppose que nous pourrions trouver un exemple invariant).

Donc @aturon et moi avons quelque peu discuté de ce problème et je voulais partager. Il y a vraiment quelques questions orthogonales ici et je veux les séparer. La première question est « quels paramètres de type/durée de vie peuvent potentiellement être utilisés dans le type caché ? » En termes de (quasi-)désucrage en default type , cela revient à "quels paramètres de type apparaissent sur le trait que nous introduisons". Ainsi, par exemple, si cette fonction :

fn foo<'a, 'b, T>() -> impl Trait { ... }

serait désucrée à quelque chose comme:

fn foo<'a, 'b, T>() -> <() as Foo<...>>::Type { ... }
trait Foo<...> {
  type Type: Trait;
}
impl<...> Foo<...> for () {
  default type Type = /* inferred */;
}

alors cette question revient à "quels paramètres de type apparaissent sur le trait Foo et son impl" ? Fondamentalement, le ... ici. Il est clair que cela inclut l'ensemble des paramètres de type qui apparaissent et sont utilisés par Trait lui-même, mais quels paramètres de type supplémentaires ? (Comme je l'ai noté précédemment, ce désucrage est fidèle à 100 %, à l'exception de la fuite de traits automatiques, et je dirais que nous devrions également divulguer des traits automatiques pour les implémentations spécialisées.)

La réponse par défaut que nous avons utilisée est "tous", donc ici ... serait 'a, 'b, T (avec tous les paramètres anonymes qui peuvent apparaître). Cela _peut_ être une valeur par défaut raisonnable, mais ce n'est pas _nécessairement_ la meilleure valeur par défaut. (Comme @arielb1 l'a souligné.)

Cela a un effet sur la relation de survie, puisque, afin de déterminer que <() as Foo<...>>::Type (se référant à une instanciation particulière et opaque de impl Trait ) survit à 'x , nous devons effectivement montrer ce ...: 'x (c'est-à-dire tous les paramètres de durée de vie et de type).

C'est pourquoi je dis qu'il ne suffit pas de considérer les paramètres de durée de vie : imaginez que nous ayons un appel à foo comme foo::<'a0, 'b0, &'c0 i32> . Cela implique que les trois durées de vie, '[abc]0 , doivent survivre à 'x -- en d'autres termes, tant que la valeur de retour est utilisée, cela prolongera les prêts de toutes les données fournies dans la fonction . Mais, comme l'a souligné @arielb1 , élision suggère que cela sera généralement plus long que nécessaire.

Donc j'imagine que ce dont nous avons besoin c'est :

  • régler un défaut raisonnable, peut-être en utilisant l'intuition d'élision ;
  • avoir une syntaxe explicite lorsque la valeur par défaut n'est pas appropriée.

@aturon a craché quelque chose comme impl<...> Trait comme syntaxe explicite, ce qui semble raisonnable. On pourrait donc écrire :

fn foo<'a, 'b, T>(...) -> impl<T> Trait { }

pour indiquer que le type caché ne fait pas référence à 'a ou 'b mais seulement T . Ou on pourrait écrire impl<'a> Trait pour indiquer que ni 'b ni T sont capturés.

En ce qui concerne les valeurs par défaut, il semble qu'avoir plus de données serait assez utile - mais la logique générale d'élision suggère que nous ferions bien de capturer tous les paramètres nommés dans le type self , le cas échéant. Par exemple, si vous avez fn foo<'a,'b>(&'a self, v: &'b [u8]) et que le type est Bar<'c, X> , alors le type de self serait &'a Bar<'c, X> et donc nous capturerions 'a , 'c et X par défaut, mais pas 'b .


Une autre note connexe est la signification d'une limite à vie. Je pense que les limites de vie du son ont une signification existante qui ne devrait pas être modifiée : si nous écrivons impl (Trait+'a) cela signifie que le type caché T survit à 'a . De même, on peut écrire impl (Trait+'static) pour indiquer qu'il n'y a pas de pointeurs empruntés présents (même si certaines durées de vie sont capturées). Lors de l'inférence du type caché T , cela impliquerait une limite à vie comme $T: 'static , où $T est la variable d'inférence que nous créons pour le type caché. Cela serait traité de la manière habituelle. Du point de vue de l'appelant, où le type caché est, eh bien, caché, la limite 'static nous permettrait de conclure que impl (Trait+'static) survit à 'static même s'il y a des paramètres de durée de vie capturés.

Ici, il se comporte exactement comme le ferait le désucrage :

fn foo<'a, 'b, T>() -> <() as Foo<'a, 'b, 'T>>::Type { ... }
trait Foo<'a, 'b, T> {
  type Type: Trait + 'static; // <-- note the `'static` bound appears here
}
impl<'a, 'b, T> Foo<...> for () {
  default type Type = /* something that doesn't reference `'a`, `'b`, or `T` */;
}

Tout cela est orthogonal à partir de l'inférence. Nous voulons toujours (je pense) ajouter la notion de contrainte "choisir parmi" et modifier l'inférence avec quelques heuristiques et, éventuellement, une recherche exhaustive (l'expérience de la RFC 1214 suggère que les heuristiques avec un repli conservateur peuvent en fait nous mener très loin ; Je ne suis pas au courant que des personnes se heurtent à des limitations à cet égard, bien qu'il y ait probablement un problème quelque part). Certes, l'ajout de limites de durée de vie comme 'static ou 'a' peut influencer l'inférence, et donc être utile, mais ce n'est pas une solution parfaite : d'une part, elles sont visibles pour l'appelant et font partie de l'API, ce qui peut ne pas être souhaité.

Options possibles :

Durée de vie explicite liée à l'élision du paramètre de sortie

Comme les objets trait aujourd'hui, les objets impl Trait ont un seul paramètre lié à la durée de vie, qui est déduit à l'aide des règles d'élision.

Inconvénient : peu ergonomique
Avantage : clair

Limites de durée de vie explicites avec élision « tout générique »

Comme les objets trait aujourd'hui, les objets impl Trait ont un seul paramètre lié à la durée de vie.

Cependant, élision crée un nouveau paramètre lié au début qui survit à tous les paramètres explicites :

fn foo<T>(&T) -> impl Foo
-->
fn foo<'total, T: 'total>(&T) -> impl Foo + 'total

Inconvénient : ajoute un paramètre lié au début

Suite.

J'ai rencontré ce problème avec impl Trait +'a et en empruntant : https://github.com/rust-lang/rust/issues/37790

Si je comprends bien ce changement (et la probabilité que cela soit faible !), alors je pense que ce code de terrain de jeu devrait fonctionner :

https://play.rust-lang.org/?gist=496ec05e6fa9d3a761df09c95297aa2a&version=nightly&backtrace=0

Tant ThingOne que ThingTwo implémentent le trait Thing . build dit qu'il retournera quelque chose qui implémente Thing , ce qu'il fait. Pourtant, il ne compile pas. Donc j'ai clairement mal compris quelque chose.

Ce « quelque chose » doit avoir un type, mais dans votre cas, vous avez deux types en conflit. @nikomatsakis a déjà suggéré de faire en sorte que cela fonctionne en général en créant par exemple ThingOne | ThingTwo lorsque des incompatibilités de type apparaissent.

@eddyb peux -tu préciser ThingOne | ThingTwo ? N'avez-vous pas besoin d'avoir Box si nous ne connaissons le type qu'à l'exécution ? Ou est-ce une sorte de enum ?

Oui, il pourrait s'agir d'un type ad hoc de type enum qui délègue les appels de méthode de trait, dans la mesure du possible, à ses variantes.

J'ai déjà voulu ce genre de chose avant aussi. Les énumérations anonymes RFC : https://github.com/rust-lang/rfcs/pull/1154

C'est un cas rare de quelque chose qui fonctionne mieux s'il est basé sur l'inférence, car si vous ne créez ces types que sur une incompatibilité, les variantes sont différentes (ce qui est un problème avec la forme généralisée).
Vous pouvez également obtenir quelque chose de ne pas avoir de correspondance de modèle (sauf dans des cas manifestement disjoints ?).
Mais le sucre de délégation de l'OMI "fonctionnerait" dans tous les cas pertinents, même si vous parvenez à obtenir un T | T .

Pourriez-vous épeler les autres moitiés implicites de ces phrases ? Je n'en comprends pas la plupart et je soupçonne que je manque un peu de contexte. Répondiez-vous implicitement aux problèmes avec les types d'union? Ce RFC est simplement des énumérations anonymes, pas des types d'union - (T|T) serait exactement aussi problématique que Result<T, T> .

Oh, peu importe, j'ai confondu les propositions (également bloqué sur le mobile jusqu'à ce que je règle mon disque dur défaillant, donc désolé de ressembler à Twitter).

Je trouve les énumérations anonymes (positionnelles, c'est- T|U != U|T dire hlist ) et génériques const (idem, avec des nombres de peano).

Mais, en même temps, si nous avions un support linguistique pour quelque chose, ce serait des types d'union, pas des énumérations anonymes. Par exemple, pas Result mais des types d'erreurs (pour éviter l'ennui des wrappers nommés pour eux).

Je ne sais pas si c'est le bon endroit pour demander, mais pourquoi un mot-clé comme impl nécessaire ? Je n'ai pas trouvé de discussion (c'est peut-être de ma faute).

Si une fonction renvoie impl Trait, son corps peut renvoyer des valeurs de tout type qui implémente Trait

Puisque

fn bar(a: &Foo) {
  ...
}

signifie "accepter une référence à un type qui implémente le trait Foo " je m'attendrais à ce que

fn bar() -> Foo {
  ...
}

pour signifier "retourner un type qui implémente le trait Foo ". Est-ce impossible?

@kud1ing la raison est de ne pas supprimer la possibilité d'avoir une fonction qui renvoie le type de taille dynamique Trait si la prise en charge des valeurs de retour de taille dynamique est ajoutée à l'avenir. Actuellement, Trait est déjà un DST valide, il n'est tout simplement pas possible de renvoyer un DST, vous devez donc le mettre en boîte pour en faire un type de taille.

EDIT : Il y a une discussion à ce sujet sur le fil RFC lié.

Eh bien, d'une part, que des valeurs de retour de taille dynamique soient ajoutées ou non, je préfère la syntaxe actuelle. Contrairement à ce qui se passe avec les objets trait, il ne s'agit pas d'un effacement de type, et toute coïncidence comme "le paramètre f: &Foo prend quelque chose qui implique Foo , alors que cela renvoie quelque chose qui implique Foo " pourrait être trompeur.

J'ai déduit de la discussion RFC qu'en ce moment impl est une implémentation d'espace réservé, et aucun impl n'est vraiment souhaité. Y a-t-il une raison pour _pas_ de vouloir un trait impl si la valeur de retour n'est pas DST ?

Je pense que la technique d'implémentation actuelle pour gérer les "fuites de traits automatiques" est problématique. Nous devrions plutôt appliquer un ordre DAG de sorte que si vous définissez un fn fn foo() -> impl Iterator , et que vous avez un appelant fn bar() { ... foo() ... } , nous devons alors vérifier le type foo() avant bar() (afin que nous sachions quel est le type caché). Si un cycle se produit, nous signalerons une erreur. C'est une position conservatrice -- nous pouvons probablement faire mieux -- mais je pense que la technique actuelle, où nous collectons les obligations d'auto-trait et les vérifions à la fin, ne fonctionne pas en général. Par exemple, cela ne fonctionnerait pas bien avec la spécialisation.

(Une autre possibilité qui pourrait être plus permissive que d'exiger un DAG strict est de vérifier le type des deux fns "ensemble" dans une certaine mesure. Je pense que c'est quelque chose à considérer seulement après que nous ayons ré-archivé un peu le système de traits impl.)

@Nercury je ne comprends pas. Vous demandez s'il y a des raisons de ne pas vouloir que fn foo() -> Trait signifie -> impl Trait ?

@nikomatsakis Oui, je demandais précisément cela, désolé pour le langage convulsé :). Je pensais que faire cela sans mot-clé impl serait plus simple, car ce comportement est exactement ce à quoi on s'attendrait (lorsqu'un type concret est renvoyé à la place du type de retour de trait). Cependant, j'ai peut-être raté quelque chose, c'est pourquoi je demandais.

La différence est que les fonctions renvoyant impl Trait renvoient toujours le même type - il s'agit essentiellement d'une inférence de type de retour. IIUC, les fonctions ne renvoyant que Trait pourraient renvoyer n'importe quelle implémentation de ce trait de manière dynamique, mais l'appelant devrait être prêt à allouer de l'espace pour la valeur de retour via quelque chose comme box foo() .

@Nercury La simple raison est que la syntaxe -> Trait a déjà un sens, nous devons donc utiliser autre chose pour cette fonctionnalité.

En fait, j'ai vu des gens s'attendre aux deux types de comportement par défaut, et ce genre de confusion revient assez souvent. Honnêtement, je préférerais que fn foo() -> Trait ne signifie rien (ou soit un avertissement par défaut) et qu'il y ait été explicite mots-clés pour le cas "un type connu au moment de la compilation que je peux choisir mais que l'appelant ne voit pas" et le cas "objet trait qui pourrait être envoyé dynamiquement à n'importe quel type implémentant Trait", par exemple fn foo() -> impl Trait contre fn foo() -> dyn Trait . Mais de toute évidence, ces navires ont navigué.

Pourquoi le compilateur ne génère-t-il pas une énumération qui contient tous les différents types de retour de la fonction, implémente le trait en passant les arguments à chaque variante et renvoie cela à la place ?

Cela contournerait la seule règle autorisée de type de retour.

@NeoLegends Faire cela manuellement est assez courant, et un peu de sucre pour cela pourrait être sympa et a été proposé dans le passé, mais c'est un troisième ensemble de sémantique complètement différent du retour de impl Trait ou d'un objet trait, donc ce n'est pas vraiment pertinent pour cette discussion.

@Ixrec Ouais, je sais que cela se fait manuellement, mais le vrai cas d'utilisation des énumérations anonymes en tant que types de retour générés par le compilateur est des types que vous ne pouvez pas épeler, comme de longues chaînes d'itérateurs ou de futurs adaptateurs.

Comment est cette sémantique différente? Les énumérations anonymes (dans la mesure où le compilateur les génère, pas selon les énumérations anonymes RFC) car les valeurs de retour n'ont vraiment de sens que s'il existe une API commune comme un trait qui fait abstraction des différentes variantes. Je suggère une fonctionnalité qui ressemble et se comporte toujours comme le trait impl ordinaire, juste avec la limite de type unique supprimée via une énumération générée par le compilateur que le consommateur de l'API ne verra jamais directement. Le consommateur ne doit toujours voir que 'impl Trait'.

Les énumérations anonymes et générées automatiquement donnent à impl Trait un coût caché qui est facile à manquer, c'est donc quelque chose à considérer.

Je soupçonne que la chose "auto enum pass-through" n'a de sens que pour les traits sûrs pour les objets. La même chose est-elle vraie pour impl Trait lui-même ?

@rpjohnst Sauf si cela, la variante de méthode réelle est dans les métadonnées de la caisse et monomorphisée sur le site d'appel. Bien entendu, cela nécessite que le passage d'une variante à une autre ne casse pas l'appelant. Et c'est peut-être trop magique.

@glaebhoerl

Je soupçonne que la chose "auto enum pass-through" n'a de sens que pour les traits sûrs pour les objets. Est-ce la même chose pour impl Trait lui-même ?

c'est un point intéressant ! J'ai débattu de la bonne façon de "desucrer" le trait impl, et j'étais en fait sur le point de suggérer que nous voulions peut-être le considérer davantage comme une "structure avec un champ privé" par opposition à la "projection de type abstrait " interprétation. Cependant, cela semble impliquer quelque chose comme la dérivation généralisée de nouveaux types, qui, bien sûr, s'est avérée notoirement malsaine en Haskell lorsqu'elle est combinée avec des familles de types . J'avoue ne pas avoir une compréhension complète de cette anomalie "dans le cache" mais il semble que nous devions être très prudents ici chaque fois que nous voulons générer automatiquement une implémentation d'un trait pour un type F<T> partir d'un impl pour T .

@nikomatsakis

Le problème est, en termes de Rust

trait Foo {
    type Output;
    fn get() -> Self::Output;
}

fn foo() -> impl Foo {
    // ...
    // what is the type of return_type::get?
}

Le tl;dr est que la dérivation généralisée de newtype a été (et est) implémentée simplement en transmute ing la vtable -- après tout, une vtable se compose de fonctions sur le type, et un type et son newtype ont la même représentation , donc ça devrait aller, non ? Mais cela ne fonctionne pas si ces fonctions utilisent également des types déterminés par un branchement au niveau du type sur l' identité (plutôt que la représentation) du type donné - par exemple, en utilisant des fonctions de type ou des types associés (ou en Haskell, les GADT). Parce qu'il n'y a aucune garantie que les représentations de ces types soient également compatibles.

Notez que ce problème n'est possible qu'en raison de l'utilisation d'une transmutation non sécurisée. Si au lieu de cela, il générait simplement le code passe-partout ennuyeux pour envelopper / déballer le newtype partout et envoyer chaque méthode à son implémentation à partir du type de base (comme certaines des propositions de délégation automatique pour Rust IIRC ?), alors le pire résultat possible serait un type erreur ou peut-être un ICE. Après tout, par construction, si vous n'utilisez pas de code dangereux, vous ne pouvez pas avoir de résultat dangereux. De même, si nous générions du code pour une sorte de "passthrough enum automatique", mais n'utilisions aucune primitive unsafe pour le faire, il n'y aurait aucun danger.

(Je ne sais pas si ou comment cela se rapporte à ma question initiale de savoir si les traits utilisés avec impl Trait , et/ou le passthrough enum automatique, devraient nécessairement être sécurisés pour les objets ?)

@rpjohnst On pourrait faire en sorte que l'enum case opt-in pour marquer le coût :

fn foo() -> enum impl Trait { ... }

C'est presque certainement de la nourriture pour un RFC différent.

@glaebhoerl ouais, j'ai passé du temps à creuser le problème et je me suis senti assez convaincu que ce ne serait pas un problème ici, au moins.

Excusez-moi si c'est quelque chose d'évident, mais j'essaie de comprendre les raisons pour lesquelles impl Trait ne peut pas apparaître dans les types de retour de méthodes de trait, ou si cela a du sens en premier lieu ? Par exemple:

trait IterInto {
    type Output;
    fn iter_into(&self) -> impl Iterator<Item=impl Into<Self::Output>>;
}

@aldanor Cela a tout à fait du sens, et AFAIK, l'intention est de faire en sorte que cela fonctionne, mais cela n'a pas encore été mis en œuvre.

C'est en quelque sorte logique, mais ce n'est pas la même caractéristique sous-jacente (cela a été beaucoup discuté d'ailleurs):

// What that trait would desugar into:
trait IterInto {
    type Output;
    type X: Into<Self::Output>;
    type Y: Iterator<Item=Self::X>;
    fn iter_into(&self) -> Self::Y;
}

// What an implementation would desugar into:
impl InterInto for FooList {
    type Output = Foo;
    // These could potentially be left unspecified for
    // a similar effect, if we want to allow that.
    type X = impl Into<Foo>;
    type Y = impl Iterator<Item=Self::X>;
    fn iter_into(&self) -> Self::Y {...}
}

Plus précisément, impl Trait dans les RHS des types associés impl Trait for Type serait similaire à la fonctionnalité implémentée aujourd'hui, en ce sens qu'elle ne peut pas être désucrée en Rust stable, alors que dans le trait ça peut être.

Je sais que c'est probablement à la fois trop tard et surtout pour le bikeshedding, mais a-t-il été documenté quelque part pourquoi le mot-clé impl été introduit ? Il me semble que nous avons déjà un moyen dans le code Rust actuel de dire "le compilateur détermine quel type va ici", à savoir _ . Ne pourrions-nous pas réutiliser ceci ici pour donner la syntaxe :

fn foo() -> _ as Iterator<Item=u8> {}

@jonhoo Ce n'est pas ce que fait la fonctionnalité, le type n'est pas celui renvoyé par la fonction, mais plutôt un "encapsuleur sémantique" qui cache tout sauf les API choisies (et les OIBIT parce que c'est pénible).

Nous pourrions autoriser certaines fonctions à déduire des types dans leurs signatures en forçant un DAG, mais une telle fonctionnalité n'a jamais été approuvée et il est peu probable qu'elle soit ajoutée à Rust, car elle toucherait à une "inférence globale".

Suggérez l'utilisation de la syntaxe @Trait pour remplacer impl Trait , comme mentionné ici .

Il est plus facile de l'étendre à d'autres positions de type et en composition comme Box<@MyTrait> ou &@MyTrait .

@Trait pour any T where T: Trait et ~Trait pour some T where T: Trait :

fn compose<T, U, V>(f: @Fn(T) -> U, g: @Fn(U) -> V) -> ~Fn(T) -> V {
    move |x| g(f(x))
}

Dans fn func(t: T) -> V , nul besoin de distinguer un t ou un v, donc comme trait.

fn compose<T, U, V>(f: @Fn(T) -> U, g: @Fn(U) -> V) -> @Fn(T) -> V {
    move |x| g(f(x))
}

fonctionne encore.

@JF-Liu Je suis personnellement opposé à ce que any et some soient fusionnés en un seul mot-clé/sigil, mais vous avez techniquement raison de dire que nous pourrions avoir un seul sigil et l'utiliser comme le impl Trait RFC.

@JF-Liu @eddyb Il y avait une raison pour laquelle les sceaux ont été supprimés du langage. Pourquoi cette raison ne s'appliquerait pas à ce cas ?

@ est également utilisé dans la recherche de motifs, non supprimé du langage.

Ce que j'avais en tête, c'est que les sceaux AFAIK étaient sur-utilisés.

Syntaxe bikesheding : Je suis profondément mécontent impl Trait notation observation de la syntaxe forte de struct C et de Stroustroup (diapositive 14) ?

Dans https://internals.rust-lang.org/t/ideas-for-making-rust-easier-for-beginners/4761 , @konstin a suggéré la <Trait> . C'est vraiment sympa, surtout dans les positions d'entrée :

fn take_iterator(iterator: <Iterator<Item=i32>>)

Je vois que cela entrera en conflit avec l'UFCS, mais peut-être que cela peut être résolu ?

Moi aussi, j'ai l'impression que l'utilisation de crochets angulaires au lieu d'impl Trait est un meilleur choix, du moins en position de type retour, par exemple :

fn returns_iter() -> <Iterator<Item=i32>> {...}
fn returns_closure() -> <FnOnce() -> bool> {...}

<Trait> conflits de syntaxe avec les génériques, considérez :

Vec<<FnOnce() -> bool>> vs Vec<@FnOnce() -> bool>

Si Vec<FnOnce() -> bool> est autorisé, alors <Trait> est une bonne idée, cela signifie l'équivalence au paramètre de type générique. Mais comme Box<Trait> est différent de Box<@Trait> , vous devez abandonner la syntaxe <Trait> .

Je préfère la syntaxe des mots clés impl car lorsque vous lisez rapidement la documentation, cela permet moins de mal lire les prototypes.
Qu'en penses-tu ?

Je viens de réaliser que j'ai proposé un sur-ensemble à ce rfc dans le fil interne (Merci à @matklad de m'avoir

Autorisez l'utilisation des traits dans les paramètres de fonction et les types de retour en les entourant de chevrons comme dans l'exemple suivant :

fn transform(iter: <Iterator>) -> <Iterator> {
    // ...
}

Le compilateur monomorphiserait alors le paramètre en utilisant les mêmes règles actuellement appliquées aux génériques. Le type de retour pourrait par exemple être dérivé de l'implémentation des fonctions. Cela signifie que vous ne pouvez pas simplement appeler cette méthode sur un Box<Trait_with_transform> ou l'utiliser sur des objets distribués dynamiquement en général, mais cela rendrait toujours les règles plus permissives. Je n'ai pas lu toutes les discussions sur les RFC, alors peut-être qu'il y a déjà une meilleure solution que j'ai manquée.

Je préfère la syntaxe des mots clés impl car lorsque vous lisez rapidement la documentation, cela permet moins de mal lire les prototypes.

Une couleur différente dans la coloration syntaxique devrait faire l'affaire.

Cet article de Stroustrup discute des choix syntaxiques similaires pour les concepts C++ dans la section 7 : http://www.stroustrup.com/good_concepts.pdf

N'utilisez pas la même syntaxe pour les génériques et les existentiels. Ce n'est pas la même chose. Les génériques permettent à l'appelant de décider quel est le type concret, tandis que (ce sous-ensemble restreint d') existentiels permet à la fonction appelée de décider quel est le type concret. Cet exemple :

fn transform(iter: <Iterator>) -> <Iterator>

devrait soit être équivalent à ceci

fn transform<T: Iterator, U: Iterator>(iter: T) -> U

ou ça devrait être équivalent à ça

fn transform(iter: impl Iterator) -> impl Iterator

Le dernier exemple ne compilera pas correctement, même la nuit, et il n'est pas réellement appelable avec le trait itérateur, mais un trait comme FromIter permettrait à l'appelant de construire une instance et de la transmettre à la fonction sans pouvoir pour déterminer le type concret de ce qu'ils passent.

Peut-être que la syntaxe devrait être similaire, mais elle ne devrait pas être la même.

Pas besoin de distinguer les (génériques) ou certains (existentiels) dans le nom du type, cela dépend de l'endroit où le type est utilisé. Lorsqu'ils sont utilisés dans des variables, des arguments et des champs de structure acceptent toujours l'un des T, lorsqu'ils sont utilisés dans le type de retour fn, obtiennent toujours une partie de T.

  • utilisez Type , &Type , Box<Type> pour les types de données concrets, répartition statique
  • utilisez @Trait , &@Trait , Box<@Trait> et le paramètre de type générique pour le type de données abstrait, répartition statique
  • utilisez &Trait , Box<Trait> pour le type de données abstrait, répartition dynamique

fn func(x: @Trait) équivaut à fn func<T: Trait>(x: T) .
fn func<T1: Trait, T2: Trait>(x: T1, y: T2) peut être simplement écrit sous la forme fn func(x: <strong i="22">@Trait</strong>, y: @Trait) .
T paramètre fn func<T: Trait>(x: T, y: T) .

struct Foo { field: <strong i="28">@Trait</strong> } équivaut à struct Foo<T: Trait> { field: T } .

Lorsqu'ils sont utilisés dans des variables, des arguments et des champs de structure acceptent toujours l'un des T, lorsqu'ils sont utilisés dans le type de retour fn, obtiennent toujours une partie de T.

Vous pouvez retourner n'importe quel trait, dès maintenant, dans Rust stable, en utilisant la syntaxe générique existante. C'est une fonctionnalité très utilisée. serde_json::de::from_slice prend &[u8] comme paramètre et renvoie T where T: Deserialize .

Vous pouvez également renvoyer de manière significative certains des traits, et c'est la fonctionnalité dont nous discutons. Vous ne pouvez pas utiliser d'existentiels pour la fonction de désérialisation, tout comme vous ne pouvez pas utiliser de génériques pour renvoyer des fermetures non encadrées. Ce sont des caractéristiques différentes.

Pour un exemple plus familier, Iterator::collect peut renvoyer n'importe quel T where T: FromIterator<Self::Item> , ce qui implique ma notation préférée : fn collect(self) -> any FromIterator<Self::Item> .

Et la syntaxe
fn foo () -> _ : Trait { ... }
pour les valeurs de retour et
fn foo (m: _1, n: _2) -> _ : Trait where _1: Trait1, _2: Trait2 { ... }
pour les paramètres ?

Pour moi vraiment, aucune des nouvelles suggestions ne se rapproche de impl Trait dans son élégance. impl est un mot-clé déjà connu de tous les programmeurs de rouille et puisqu'il est utilisé pour implémenter des traits, il suggère en fait ce que la fonctionnalité fait toute seule.

Oui, s'en tenir aux mots-clés existants me semble idéal ; J'aimerais voir impl pour les existentiels et for pour les universels.

Je suis personnellement opposé à ce que any et some soient amalgamés en un seul mot-clé/sigil

@eddyb Je ne considérerais pas cela comme une confusion. Il découle naturellement de la règle :

((∃ T . F⟨T⟩) → R)  →  ∀ T . (F⟨T⟩ → R)

Edit : c'est à sens unique, pas un isomorphisme.


Non lié : existe-t-il une proposition connexe pour autoriser également impl Trait dans d'autres positions covariantes telles que

~ rouillefn foo(rappel : F) -> Roù F: FnOnce(impl SomeTrait) -> R {rappel (créer_quelquechose())}~

Pour le moment , ce n'est pas une fonctionnalité nécessaire, car vous pouvez toujours mettre une heure concrète pour impl SomeTrait , ce qui nuit à la lisibilité mais n'est pas grave sinon.

Mais si la fonctionnalité RFC 1522 se stabilise, il serait alors impossible d'attribuer une signature de type à des programmes tels que ceux ci-dessus si create_something aboutit à impl SomeTrait (au moins sans le boxer). Je pense que c'est problématique.

@Rufflewind Dans le monde réel, les choses ne sont pas si claires, et cette fonctionnalité est une marque très spécifique d'existentiels (Rust en a plusieurs maintenant).

Mais même dans ce cas, tout ce que vous avez est l'utilisation de la covariance pour déterminer ce que impl Trait signifie à l'intérieur et à l'extérieur des arguments de fonction.

Cela ne suffit pas pour :

  • en utilisant le contraire de la valeur par défaut
  • lever l'ambiguïté à l'intérieur du type d'un champ (où les deux any et some sont également souhaitables)

@Rufflewind Cela semble être le mauvais bracketing pour ce qu'est impl Trait . Je sais que Haskell exploite cette relation pour n'utiliser que le mot-clé forall pour représenter à la fois les universaux et les existentiels, mais cela ne fonctionne pas dans le contexte dont nous discutons.

Prenons cette définition, par exemple :

fn foo(x: impl ArgTrait) -> impl ReturnTrait { ... }

Si nous utilisons la règle selon laquelle " impl dans les arguments est universel, impl dans les types de retour est existentiel", alors le type de l'élément de fonction foo est logiquement celui-ci (dans notation de type composée):

forall<T: ArgTrait>(exists<R: ReturnTrait>(fn(T) -> R))

Traiter naïvement impl comme ne signifiant techniquement que l'universel ou uniquement l'existentiel et laisser la logique fonctionner elle-même ne fonctionne pas vraiment. Vous obtiendrez soit ceci :

forall<T: ArgTrait, R: ReturnTrait>(fn(T) -> R)

Ou ca:

exists<T: ArgTrait, R: ReturnTrait>(fn(T) -> R)

Et ni l'un ni l'autre ne se réduit à ce que nous voulons par des règles logiques. Donc , en fin de compte any / some ne capturons une distinction importante que vous ne pouvez pas capturer avec un seul mot - clé. Il y a même des exemples raisonnables dans std où vous voulez des universaux en position de retour. Par exemple, cette méthode Iterator :

fn collect<B>(self) -> B where B: FromIterator<Self::Item>;
// is equivalent to
fn collect(self) -> any FromIterator<Self::Item>;

Et il n'y a aucun moyen de l'écrire avec impl et la règle argument/retour.

tl;dr ayant impl dénotant contextuellement soit universel soit existentiel lui donne vraiment deux significations distinctes.


Pour référence, dans ma notation, la relation forall/ exists mentionnée par

fn(exists<T: Trait>(T)) -> R === forall<T: Trait>(fn(T) -> R)

Ce qui est lié au concept d'objets traits (existentiels) équivalents aux génériques (universels), mais pas à cette question impl Trait .

Cela dit, je ne suis plus fortement en faveur de any / some . Je voulais être précis sur ce dont nous parlons, et any / some ont cette gentillesse théorique et visuelle, mais je serais bien d'utiliser impl avec le contexte régner. Je pense que cela couvre tous les cas courants, cela évite les problèmes de grammaire contextuelle des mots clés et nous pouvons passer aux paramètres de type nommés pour le reste.

Sur cette note, pour correspondre à la généralité complète des universaux, je pense que nous aurons éventuellement besoin d'une syntaxe pour les existentiels nommés, qui permette des clauses where arbitraires et la possibilité d'utiliser le même existentiel à plusieurs endroits dans la signature.

En résumé, je serais content de :

  • impl Trait comme raccourci pour les universaux et les existentiels (contextuellement).
  • Paramètres de type nommés comme le longhand entièrement général pour les universaux et les existentiels. (Moins souvent nécessaire.)

Traiter naïvement impl comme ne signifiant techniquement que l'universel ou uniquement l'existentiel et laisser la logique s'accomplir elle-même ne fonctionne pas réellement. Vous obtiendrez soit ceci :

@solson Pour moi, une traduction «naïve» entraînerait la quantification des quantificateurs existentiels juste à côté du type. D'où

~ rouille(impl MonTrait)~

n'est que du sucre syntaxique pour

~ rouille(existeT)~

qui est une simple transformation locale. Ainsi, une traduction naïve obéissant à la règle « impl est toujours une règle existentielle » donnerait :

~ rouillefn(existeT) -> (existeR)~

Ensuite, si vous retirez le quantificateur de l'argument de la fonction, il devient

~ rouillepourfn(T) -> (existeR)~

Ainsi, même si T est toujours existentiel par rapport à lui-même, il apparaît comme universel par rapport à l'ensemble du type de fonction.


IMO, je pense que impl pourrait tout aussi bien devenir le mot-clé de facto pour les types existentiels. À l'avenir, on pourrait peut-être construire des types existentiels plus compliqués comme :

~~rouille(impl(Vec, T))~ ~

par analogie avec les types universels (via HRTB)

~ rouille(pour<'a> FnOnce(&'a T))~

@Rufflewind Cette vue ne fonctionne pas car fn(T) -> (exists<R: ReturnTrait>(R)) n'est pas logiquement équivalent à exists<R: ReturnTrait>(fn(T) -> R) , ce que signifie réellement le type impl Trait retour

(Du moins pas dans la logique constructive habituellement appliquée aux systèmes de types, où le témoin spécifique choisi pour un existentiel est pertinent. Le premier implique que la fonction pourrait choisir différents types à renvoyer en fonction, disons, des arguments, tandis que le second implique qu'il y a un type spécifique pour toutes les invocations de la fonction, comme c'est le cas dans impl Trait .)

J'ai aussi l'impression qu'on s'éloigne un peu. Je pense que le impl contextuel est un bon compromis à faire, et je ne pense pas qu'atteindre ce type de justification soit nécessaire ou particulièrement utile (nous n'enseignerions certainement pas la règle en termes de ce type de connexion logique ).

@solson Oui, vous avez raison : les existentiels ne peuvent pas être mis à l'eau. Celui-ci ne tient pas en général :

(T → ∃R. f(R))  ⥇  ∃R. T → f(R)

alors que ceux-ci sont valables en général :

(∃R. T → f(R))  →   T → ∃R. f(R)
(∀A. g(A) → T)  ↔  ((∃A. g(A)) → T)

Le dernier est responsable de la réinterprétation des existentiels dans les arguments en tant que génériques.

Edit: Oops, (∀A. g(A) → T) → (∃A. g(A)) → T fait main.

J'ai posté une RFC avec une proposition détaillée pour étendre et stabiliser impl Trait . Il s'appuie sur une grande partie de la discussion sur ce sujet et sur des sujets antérieurs.

A noter que https://github.com/rust-lang/rfcs/pull/1951 a été accepté.

Quel est le statut à ce sujet actuellement? Nous avons un RFC qui a atterri, nous avons des gens qui utilisent la mise en œuvre initiale, mais je ne sais pas exactement quels éléments sont à faire.

Il a été trouvé dans #43869 que la fonction -> impl Trait ne prend pas en charge un corps purement divergent :

fn do_it_later_but_cannot() -> impl Iterator<Item=u8> { //~ ERROR E0227
    unimplemented!()
}

Est-ce attendu (puisque ! n'implique pas Iterator ), ou est-il considéré comme un bogue ?

Qu'en est-il de la définition des types inférés, qui pourraient non seulement être utilisés comme valeurs de retour, mais comme tout (je suppose) pour lequel un type peut être utilisé actuellement ?
Quelque chose comme:
type Foo: FnOnce() -> f32 = #[infer];
Ou avec un mot-clé :
infer Foo: FnOnce() -> f32;

Le type Foo pourrait alors être utilisé comme type de retour, type de paramètre ou tout autre type pour lequel un type peut être utilisé, mais il serait illégal de l'utiliser à deux endroits différents qui nécessitent un type différent, même si cela type implémente FnOnce() -> f32 dans les deux cas. Par exemple, les éléments suivants ne compileraient pas :

infer Foo: FnOnce() -> f32;

fn return_closure() -> Foo {
    || 0.1
}

fn return_closure2() -> Foo {
    || 0.2
}

fn main() {
    println!("{:?}, {:?}", return_closure()(), return_closure2()());
}

Cela ne devrait pas être compilé car même si les types de retour de return_closure et return_closure2 sont tous les deux FnOnce() -> f32 , leurs types sont en fait différents, car il n'y a pas deux fermetures du même type dans Rust . Pour que ce qui précède soit compilé, vous devez donc définir deux types inférés différents :

infer Foo: FnOnce() -> f32;
infer Foo2: FnOnce() -> f32; //Added this line

fn return_closure() -> Foo {
    || 0.1
}

fn return_closure2() -> Foo2 { //Changed Foo to Foo2
    || 0.2
}

fn main() {
    println!("{:?}, {:?}", return_closure()(), return_closure2()());
}

Je pense que ce qui se passe ici est assez évident après avoir vu le code, même si vous ne saviez pas à l'avance ce que fait le mot-clé infer, et il est très flexible.

Le mot-clé infer (ou macro) indiquerait essentiellement au compilateur de déterminer quel est le type, en fonction de l'endroit où il est utilisé. Si le compilateur n'est pas en mesure de déduire le type, il générera une erreur, cela pourrait se produire lorsqu'il n'y a pas assez d'informations pour préciser de quel type il doit s'agir (si le type inféré n'est utilisé nulle part, par exemple, bien que peut-être vaut-il mieux faire de ce cas spécifique un avertissement), ou lorsqu'il est impossible de trouver un type qui s'adapte partout où il est utilisé (comme dans l'exemple ci-dessus).

@cramertj Ahh, c'est pourquoi ce problème est devenu si silencieux.

Donc, @cramertj me demandait comment je pensais qu'il serait préférable de résoudre le problème des régions à liaison tardive qu'ils ont rencontrées dans leur RP. Mon point de vue est que nous voulons probablement « réoutiller » un peu notre implémentation pour essayer d'anticiper le modèle anonymous type Foo .

Pour le contexte, l'idée est à peu près que

fn foo<'a, 'b, T, U>() -> impl Debug + 'a

serait (en quelque sorte) dessucré à quelque chose comme ça

anonymous type Foo<'a, T, U>: Debug + 'a
fn foo<'a, 'b, T, U>() -> Foo<'a, T, U>

Notez que sous cette forme, vous pouvez voir quels paramètres génériques sont capturés car ils apparaissent en tant qu'arguments de Foo -- notamment, 'b n'est pas capturé, car il n'apparaît pas dans la référence de trait dans de toute façon, mais les paramètres de type T et U sont toujours.

Quoi qu'il en soit, actuellement dans le compilateur, lorsque vous avez une référence impl Debug , nous créons un def-id qui -- effectivement -- représente ce type anonyme. Ensuite, nous avons la requête generics_of , qui calcule ses paramètres génériques. À l'heure actuelle, cela renvoie le même que le contexte "englobant" - c'est-à-dire la fonction foo . C'est ce que nous voulons changer.

De "l'autre côté", c'est-à-dire dans la signature de foo , nous représentons impl Foo comme un TyAnon . C'est fondamentalement juste -- le TyAnon représente la référence à Foo que nous voyons dans le désucrage ci-dessus. Mais la façon dont nous obtenons les "substs" pour ce type est d'utiliser la fonction "identity" , ce qui est clairement faux - ou du moins ne généralise pas.

Donc, en particulier, il y a une sorte de "violation d'espace de noms" qui a lieu ici. Lorsque nous générons les substs "d'identité" pour un élément, cela nous donne normalement les substitutions que nous utiliserions lors de la vérification de type de cet élément - c'est-à-dire avec tous ses paramètres génériques dans la portée. Mais dans ce cas, nous créons la référence à Foo qui apparaît à l'intérieur de la fonction foo() , et nous voulons donc que les paramètres génériques de foo() apparaissent dans Substs , pas ceux de Foo . Cela fonctionne parce qu'en ce moment ce ne sont qu'une seule et même chose, mais ce n'est pas vraiment juste .

Je pense que ce que nous devrions faire est quelque chose comme ceci:

Premièrement, lorsque nous calculons les paramètres de type générique de Foo (c'est-à-dire le type anonyme lui-même), nous commençons à construire un nouvel ensemble de génériques. Naturellement, cela inclurait les types. Mais pendant des vies, nous franchirions les limites des traits et identifierions chacune des régions qui apparaissent à l'intérieur. C'est très similaire à ce code existant que cramertj a écrit , sauf que nous ne voulons pas accumuler de def-ids, car toutes les régions

Je pense que ce que nous voulons faire, c'est accumuler l'ensemble des régions qui apparaissent et les mettre dans un certain ordre, et également suivre les valeurs de ces régions du point de vue de foo() . C'est un peu ennuyeux de faire cela, car nous n'avons pas de structure de données uniforme qui représente une région logique. (Nous avions l'habitude d'avoir la notion de FreeRegion , ce qui aurait presque fonctionné, mais nous n'utilisons plus FreeRegion pour les choses à reliure anticipée, seulement pour les choses à reliure tardive.)

L'option la plus simple et la meilleure serait peut-être d'utiliser simplement un Region<'tcx> , mais vous devrez déplacer les profondeurs d'index de debruijn au fur et à mesure que vous allez "annuler" tous les classeurs introduits. C'est peut-être le meilleur choix cependant.

Donc, fondamentalement, lorsque nous obtenons des rappels dans visit_lifetime , nous les transformerions en Region<'tcx> exprimés dans la profondeur initiale (nous devrons suivre lorsque nous parcourrons les classeurs). Nous les accumulerons dans un vecteur, en éliminant les doublons.

Lorsque nous avons terminé, nous avons deux choses :

  • Tout d'abord, pour chaque région du vecteur, nous devons créer un paramètre de région générique. Ils peuvent tous avoir des noms anonymes ou autre, cela n'a pas beaucoup d'importance (bien que nous ayons peut-être besoin d'eux pour avoir des def-ids ou quelque chose comme ça...? Je dois regarder les structures de données RegionParameterDef ...) .
  • Deuxièmement, les régions du vecteur sont également les éléments que nous voulons utiliser pour les "substs".

OK, désolé si c'est un mystère. Je ne sais pas trop comment le dire plus clairement. Quelque chose dont je ne suis pas sûr cependant - pour le moment, je pense que notre gestion des régions est assez complexe, alors peut-être qu'il y a un moyen de refactoriser les choses pour les rendre plus uniformes ? Je parierais 10 $ que @eddyb a quelques réflexions ici. ;)

@nikomatsakis Je pense que cela ressemble en grande partie à ce que j'ai dit à @cramertj , mais plus étoffé !

J'ai pensé aux impl Trait existentiels et j'ai rencontré un cas curieux où je pense que nous devons procéder avec prudence. Considérez cette fonction :

trait Foo<T> { }
impl Foo<()> for () { }
fn foo() -> impl Foo<impl Debug> {
  ()
}

Comme vous pouvez le valider sur play , ce code se compile aujourd'hui. Cependant, si nous creusons dans ce qui se passe, cela met en évidence quelque chose qui présente un danger de « compatibilité prospective » qui me préoccupe.

Plus précisément, il est clair comment nous déduisons le type qui est renvoyé ici ( () ). Il est moins clair comment on déduit le type du paramètre impl Debug . C'est-à-dire que vous pouvez considérer cette valeur de retour comme étant quelque chose comme -> ?T?T: Foo<?U> . Nous devons déduire les valeurs de ?T et ?U basant uniquement sur le fait que ?T = () .

À l'heure actuelle, nous le faisons en tirant parti du fait qu'il n'existe qu'un seul impl. Cependant, il s'agit d'une propriété fragile. Si un nouvel impl est ajouté, le code ne sera plus compilé , car maintenant nous ne pouvons pas déterminer de manière unique ce que doit être ?U .

Cela peut se produire dans de nombreux scénarios dans Rust - ce qui est assez préoccupant, mais orthogonal - mais il y a quelque chose de différent dans le cas impl Trait . Dans le cas de impl Trait , nous n'avons aucun moyen pour l'utilisateur d'ajouter des annotations de type pour guider l'inférence ! Nous n'avons pas non plus vraiment de plan pour une telle voie. La seule solution est de changer l'interface fn en impl Foo<()> ou autre chose d'explicite.

À l'avenir, en utilisant abstract type , on pourrait imaginer permettre aux utilisateurs de donner explicitement la valeur cachée (ou peut-être simplement des indices incomplets, en utilisant _ ), ce qui pourrait alors aider à l'inférence, tout en gardant à peu près le même interface publique

abstract type X: Debug = ();
fn foo() -> impl Foo<X> {
  ()
}

Néanmoins, je pense qu'il serait prudent d'éviter de stabiliser les utilisations « imbriquées » du trait impl existentiel, sauf dans les liaisons de type associées (par exemple, impl Iterator<Item = impl Debug> ne souffre pas de ces ambiguïtés).

Dans le cas d'impl Trait, nous n'avons aucun moyen pour l'utilisateur d'ajouter des annotations de type pour guider l'inférence ! Nous n'avons pas non plus vraiment de plan pour une telle voie.

Peut-être que cela pourrait ressembler à UFCS ? par exemple <() as Foo<()>> -- ne pas changer le type comme un simple as , juste le désambiguïser. Cette syntaxe est actuellement invalide car elle s'attend à ce que :: et plus suive.

Je viens de trouver un cas intéressant concernant l'inférence de type avec impl Trait pour Fn :
Le code suivant compile très bien :

fn op(s: &str) -> impl Fn(i32, i32) -> i32 {
    match s {
        "+" => ::std::ops::Add::add,
        "-" => ::std::ops::Sub::sub,
        "<" => |a,b| (a < b) as i32,
        _ => unimplemented!(),
    }
}

Si nous commentons la sous-ligne, une erreur de compilation est renvoyée :

error[E0308]: match arms have incompatible types
 --> src/main.rs:4:5
  |
4 | /     match s {
5 | |         "+" => ::std::ops::Add::add,
6 | | //         "-" => ::std::ops::Sub::sub,
7 | |         "<" => |a,b| (a < b) as i32,
8 | |         _ => unimplemented!(),
9 | |     }
  | |_____^ expected fn item, found closure
  |
  = note: expected type `fn(_, _) -> <_ as std::ops::Add<_>>::Output {<_ as std::ops::Add<_>>::add}`
             found type `[closure@src/main.rs:7:16: 7:36]`
note: match arm with an incompatible type
 --> src/main.rs:7:16
  |
7 |         "<" => |a,b| (a < b) as i32,
  |                ^^^^^^^^^^^^^^^^^^^^

error: aborting due to previous error

@oberien Cela ne semble pas lié à impl Trait -- c'est vrai pour l'inférence en général. Essayez cette légère modification de votre exemple :

fn main() {
    let _: i32 = (match "" {
        "+" => ::std::ops::Add::add,
        //"-" => ::std::ops::Sub::sub,
        "<" => |a,b| (a < b) as i32,
        _ => unimplemented!(),
    })(5, 5);
}

On dirait que c'est fermé maintenant :

ICE lors de l'interaction avec élision

Une chose que je ne vois pas répertoriée dans ce numéro ou dans la discussion est la possibilité de stocker des fermetures et des générateurs - qui ne sont pas fournis par l'appelant - dans des champs struct. Pour le moment, c'est possible mais ça a l'air moche : vous devez ajouter un paramètre de type à la structure pour chaque champ de fermeture/générateur, puis dans la signature de la fonction constructeur, remplacez ce paramètre de type par impl FnMut/impl Generator . Voici un exemple , et ça marche, ce qui est plutôt cool ! Mais cela laisse beaucoup à désirer. Ce serait bien mieux si vous pouviez vous débarrasser du paramètre de type :

struct Counter(impl Generator<Yield=i32, Return=!>);

impl Counter {
    fn new() -> Counter {
        Counter(|| {
            let mut x: i32 = 0;
            loop {
                yield x;
                x += 1;
            }
        })
    }
}

impl Trait n'est peut-être pas la bonne façon de procéder - probablement des types abstraits, si j'ai lu et compris correctement la RFC 2071. Ce dont nous avons besoin, c'est de quelque chose que nous pouvons écrire dans la définition de la structure afin que le type réel ( [generator@src/main.rs:15:17: 21:10 _] ) puisse être déduit.

Les types abstraits

abstract type MyGenerator: Generator<Yield = i32, Return = !>;

pub struct Counter(MyGenerator);

impl Counter {
    pub fn new() -> Counter {
        Counter(|| {
            let mut x: i32 = 0;
            loop {
                yield x;
                x += 1;
            }
        })
    }
}

Existe-t-il un chemin de secours s'il s'agit du impl Generator de quelqu'un d'autre que je veux mettre dans ma structure, mais qu'il n'a pas créé de abstract type à utiliser ?

@scottmcm Vous pouvez toujours déclarer votre propre abstract type :

// library crate:
fn foo() -> impl Generator<Yield = i32, Return = !> { ... }

// your crate:
abstract type MyGenerator: Generator<Yield = i32, Return = !>;

pub struct Counter(MyGenerator);

impl Counter {
    pub fn new() -> Counter {
        let inner: MyGenerator = foo();
        Counter(inner)
    }
}

@cramertj Attends, les types abstraits sont déjà tous les soirs ?! Où est le RP ?

@alexreg Non, ils ne le sont pas.

Edit : Salutations, visiteurs du futur ! Le problème ci-dessous a été résolu.


Je voudrais attirer l'attention sur ce cas d'utilisation génial qui apparaît dans #47348

use ::std::ops::Sub;

fn test(foo: impl Sub) -> <impl Sub as Sub>::Output { foo - foo }

Le retour d'une projection sur impl Trait comme celui-ci devrait-il même être autorisé ? (parce qu'actuellement, __c'est.__)

Je n'ai pu localiser aucune discussion sur une utilisation comme celle-ci, ni trouver de cas de test pour cela.

@ExpHP Hum. Cela semble problématique, pour la même raison que impl Foo<impl Bar> est problématique. Fondamentalement, nous n'avons aucune contrainte réelle sur le type en question - seulement sur les choses qui en sont projetées.

Je pense que nous voulons réutiliser la logique autour des "paramètres de type contraint" de impls. En bref, spécifier le type de retour devrait "contraindre" le impl Sub . La fonction dont je parle est celle-ci :

https://github.com/rust-lang/rust/blob/a0dcecff90c45ad5d4eb60859e22bb3f1b03842a/src/librustc_typeck/constrained_type_params.rs#L89 -L93

Petit triage pour les personnes qui aiment les cases à cocher :

  • #46464 est fait -> case à cocher
  • #48072 est fait -> case à cocher

@rfcbot fcp fusionner

Je propose que nous stabilisions les fonctionnalités conservative_impl_trait et universal_impl_trait , avec un changement en attente (un correctif pour https://github.com/rust-lang/rust/issues/46541).

Tests qui documentent la sémantique actuelle

Les tests de ces fonctionnalités sont disponibles dans les répertoires suivants :

run-pass/impl-trait
ui/impl-trait
échec-compilation/trait-impl

Questions résolues pendant la mise en œuvre

Les détails de l'analyse de impl Trait ont été résolus dans la RFC 2250 et implémentés dans https://github.com/rust-lang/rust/pull/45294.

impl Trait a été banni de la position de type imbriqué non associé et de certaines positions de chemin qualifié afin d'éviter toute ambiguïté. Cela a été implémenté dans https://github.com/rust-lang/rust/pull/48084.

Fonctionnalités instables restantes

Après cette stabilisation, il sera possible d'utiliser impl Trait en position d'argument et en position de retour des fonctions non-trait. Cependant, l'utilisation de impl Trait n'importe où dans la syntaxe Fn est toujours interdite afin de permettre une future itération de conception. De plus, la spécification manuelle des paramètres de type des fonctions qui utilisent impl Trait en position d'argument n'est pas autorisée.

Le membre de l'équipe @cramertj a proposé de fusionner cela. L'étape suivante est l'examen par le reste des équipes taguées :

  • [x] @aturon
  • [x] @cramertj
  • [x] @eddyb
  • [x] @nikomatsakis
  • [x] @nrc
  • [x] @pnkfelix
  • [x] @sansbateaux

Aucune préoccupation actuellement répertoriée.

Une fois qu'une majorité de réviseurs approuvera (et aucun ne s'y opposera), cela entrera dans sa dernière période de commentaires. Si vous repérez un problème majeur qui n'a été soulevé à aucun moment de ce processus, veuillez en parler !

Consultez ce document pour plus d'informations sur les commandes que les membres de l'équipe tagués peuvent me donner.

Après cette stabilisation, il sera possible d'utiliser impl Trait en position argument et en position retour des fonctions non trait. Cependant, l'utilisation de impl Trait n'importe où dans la syntaxe Fn est toujours interdite afin de permettre une future itération de conception. De plus, la spécification manuelle des paramètres de type des fonctions qui utilisent impl Trait en position d'argument n'est pas autorisée.

Quel est le statut de l'utilisation de impl Trait dans les positions d'argument/de retour dans les fonctions de trait, ou dans la syntaxe Fn, d'ailleurs ?

@alexreg La position impl Trait retour impl Trait dans les traits n'est bloquée sur aucune caractéristique technique à ma connaissance, mais elle n'était pas explicitement autorisée dans la RFC, elle a donc été omise pour le moment.

impl Trait dans la position d'argument de Fn syntaxe est bloquée sur HRTB au niveau du type, car certaines personnes pensent que T: Fn(impl Trait) devrait dessugar à T: for<X: Trait> Fn(X) . impl Trait dans la position de retour de Fn syntaxe n'est pas bloquée pour une raison technique à ma connaissance, mais elle a été interdite dans la RFC en attendant d'autres travaux de conception - je m'attendrais à voir un autre RFC ou au moins un FCP distinct avant de le stabiliser.

@cramertj D'accord, merci pour la mise à jour. Espérons que nous pourrons voir ces deux fonctionnalités qui ne sont bloquées sur rien obtenir le feu vert bientôt, après quelques discussions. Le désucrage prend tout son sens, en position argument, un argument foo: TT: Trait équivaut à foo: impl Trait , sauf erreur de ma part.

Préoccupation : https://github.com/rust-lang/rust/issues/34511#issuecomment -322340401 est toujours la même. Est-il possible d'autoriser ce qui suit ?

fn do_it_later_but_cannot() -> impl Iterator<Item=u8> { //~ ERROR E0227
    unimplemented!()
}

@kennytm Non, ce n'est pas possible pour le moment. Cette fonction renvoie ! , qui n'implémente pas le trait que vous avez fourni, et nous n'avons pas non plus de mécanisme pour le convertir en un type approprié. C'est malheureux, mais il n'y a pas de moyen facile de le corriger pour le moment (à part la mise en œuvre de plus de traits pour ! ). Il est également rétrocompatible à corriger à l'avenir, car le faire fonctionner permettrait de compiler strictement plus de code.

La question du turbofish n'a été résolue qu'à moitié. Nous devrions au moins mettre en garde contre impl Trait dans les arguments des fonctions effectivement publiques, en considérant que impl Trait dans les arguments est un type privé pour le nouveau private in public check .

La motivation est d'empêcher les bibliothèques de casser les turbofish des utilisateurs en changeant un argument générique explicite en impl Trait . Nous n'avons pas encore de bon guide de référence pour les bibliothèques pour savoir ce qui est et n'est pas un changement de rupture et il est très peu probable que les tests détectent cela. Cette question n'a pas été suffisamment discutée, si nous souhaitons stabiliser avant de décider pleinement, nous devrions au moins pointer l'arme loin du pied des auteurs de lib.

La motivation est d'empêcher les bibliothèques de casser les turbofish des utilisateurs en changeant un argument générique explicite en impl Trait .

J'espère que lorsque cela commencera à se produire et que les gens commenceront à se plaindre, les membres de l'équipe lang qui sont actuellement dans le doute seront convaincus que impl Trait devrait prendre en charge la fourniture explicite d'arguments de type avec turbofish.

@leodasvacas

La question du turbofish n'a été résolue qu'à moitié. Nous devrions au moins mettre en garde sur impl Trait dans les arguments des fonctions effectivement publiques, considérant que impl Trait dans les arguments est un type privé pour le nouveau private in public check.

Je ne suis pas d'accord, cela a été résolu. Nous interdisons complètement les turbofish pour ces fonctions pour le moment. Changer la signature d'une fonction publique pour utiliser impl Trait au lieu de paramètres génériques explicites est un changement décisif.

Si nous autorisons les turbofish pour ces fonctions à l'avenir, cela ne permettra probablement que de spécifier des paramètres de type non impl Trait .

:bell: Ceci entre maintenant dans sa dernière période de commentaires , selon l' examen ci-dessus . :cloche:

Je dois ajouter que je ne veux pas me stabiliser jusqu'à ce que https://github.com/rust-lang/rust/pull/49041 atterrisse. (Mais j'espère que ce sera bientôt.)

Donc, #49041 contient un correctif pour #46541, mais ce correctif a plus d'impact que je ne l'avais prévu - par exemple, le compilateur ne démarre pas maintenant - et cela me donne une mesure de pause sur le bon cours ici. Le problème dans # 49041 est que nous pourrions accidentellement permettre à des vies de s'échapper que nous n'étions pas censés le faire. Voici comment cela se manifeste dans le compilateur. Nous pourrions avoir une méthode comme celle-ci :

impl TyCtxt<'cx, 'gcx, 'tcx>
where 'gcx: 'tcx, 'tcx: 'cx
{
    fn foos(self) -> impl Iterator<Item = &'tcx Foo> + 'cx {
        /* returns some type `Baz<'cx, 'gcx, 'tcx>` that captures self */
    }
}

L'élément clé ici est que TyCtxt est invariant avec 'tcx et 'gcx , ils doivent donc apparaître dans le type de retour. Et pourtant, seuls 'cx et 'tcx apparaissent dans les limites du trait impl, donc seules ces deux durées de vie sont censées être "capturées". L'ancien compilateur acceptait cela parce que 'gcx: 'cx , mais ce n'est pas vraiment correct si vous pensez au désucrage que nous avons en tête. Ce désucrage créerait un type abstrait comme celui-ci :

abstract type Foos<'cx, 'tcx>: Iterator<Item = &'tcx Foo> + 'cx;

et pourtant la valeur de ce type abstrait serait Baz<'cx, 'gcx, 'tcx> -- mais 'gcx n'est pas dans la portée !

La solution de contournement ici est que nous devons nommer 'gcx dans les limites. C'est assez ennuyeux à faire ; nous ne pouvons pas utiliser 'cx + 'gcx . Nous pouvons, je suppose, faire un trait factice :

trait Captures<'a> { }
impl<T: ?Sized> Captures<'a> for T { }

puis retournez quelque chose comme ceci impl Iterator<Item = &'tcx Foo> + Captures<'gcx> + Captures<'cx> .

Quelque chose que j'ai oublié de noter : si le type de retour déclaré était dyn Iterator<Item = &'tcx Foo> + 'cx , ce serait ok, car les types dyn sont censés effacer des vies. Par conséquent, je ne pense pas qu'il y ait d'erreur possible ici, en supposant que vous ne puissiez rien faire de problématique avec un impl Trait qui ne serait pas possible avec un dyn Trait .

On pourrait vaguement imaginer l'idée que la valeur du type abstrait est une semblable existentielle : exists<'gcx> Baz<'cx, 'gcx, 'tcx> .

Il me semble cependant correct de stabiliser un sous-ensemble conservateur (ce qui exclut les fns ci-dessus) et de revoir cela comme une extension possible plus tard, une fois que nous aurons décidé comment nous voulons y penser.

MISE À JOUR : Pour clarifier mon sens à propos des traits de dyn : je dis qu'ils peuvent « cacher » une vie comme 'gcx tant que la limite ( 'cx , ici) garantit que 'gcx sera toujours en ligne partout où le dyn Trait est utilisé.

@nikomatsakis C'est un exemple intéressant, mais je ne pense pas que cela change le calcul de base ici, c'est-à-dire que nous voulons que toutes les durées de vie pertinentes soient claires à partir du seul type de retour.

Le trait Captures semble être une bonne approche légère pour cette situation. Il semble que cela pourrait entrer dans std::marker comme instable pour le moment ?

@nikomatsakis Votre commentaire de suivi m'a fait réaliser que je n'avais pas tout à fait rassemblé toutes les pièces ici pour comprendre pourquoi vous pourriez vous attendre à éliminer le 'gcx dans ce cas, c'est- 'gcx dire que

Mon opinion personnelle est que https://github.com/rust-lang/rust/issues/46541 n'est pas vraiment un bogue - c'est le comportement auquel je m'attendrais, je ne vois pas comment cela pourrait être rendu malsain, et c'est pénible à contourner. IMO, il devrait être possible de retourner un type qui implémente Trait et survit à la durée 'a vie impl Trait + 'a , quelles que soient les autres durées de vie qu'il contient. Cependant, je suis d'accord pour stabiliser une approche plus conservatrice pour commencer si c'est ce que @rust-lang/lang préfère.

(Une autre chose à clarifier : la seule fois où vous obtiendrez des erreurs avec le correctif dans # 49041, c'est lorsque le type caché est invariant par rapport à la durée 'gcx vie manquante relativement rarement.)

@cramertj

Mon opinion personnelle est que #46541 n'est pas vraiment un bug - c'est le comportement auquel je m'attendrais, je ne vois pas comment cela pourrait être déformé, et c'est difficile à contourner.

Je suis sympathique à ce POV, mais je suis réticent à stabiliser quelque chose pour lequel nous ne comprenons pas comment le déshydrater (par exemple, parce qu'il semble reposer sur une vague notion de vies existentielles).

@rfcbot concerne les sites à retours multiples

Je voudrais enregistrer une dernière préoccupation avec le trait d'impl existentiel. Une fraction substantielle des fois où je souhaite utiliser le trait impl, je souhaite en fait renvoyer plusieurs types. Par example:

fn foo(empty: bool) -> impl Iterator<Item = u32> {
    if empty { None.into_iter() } else { &[1, 2, 3].cloned() }
}

Bien sûr, cela ne fonctionne pas aujourd'hui, et il est définitivement hors de portée de le faire fonctionner. Cependant, la façon dont fonctionne trait impl maintenant, nous fermons effectivement la porte à jamais le travail (avec cette syntaxe). En effet, actuellement, vous pouvez accumuler des contraintes à partir de plusieurs sites de retour :

fn foo(empty: bool) -> (impl Debug, impl Debug) {
    if empty { return (22, Default::default()); }
    return (Default::default(), false);
}

Ici, le type inféré est (i32, bool) , où le premier return contraint la partie i32 et le second return contraint la partie bool .

Cela implique que nous ne pourrions jamais prendre en charge les cas où les deux instructions return ne s'unifient pas (comme dans mon premier exemple) - sinon ce serait très ennuyeux de le faire.

Je me demande si nous devrions mettre une contrainte qui exige que chaque return (en général, chaque source d'une contrainte) soit entièrement spécifié indépendamment ? (Et on les unifie après coup ?)

Cela rendrait mon deuxième exemple illégal et nous laisserait la possibilité de soutenir potentiellement le premier cas à un moment donné dans le futur.

@rfcbot résout les sites de retour multiples

J'ai donc pas mal parlé à . Nous discutions de l'idée de rendre instable le "retour anticipé" pour impl Trait , afin que nous puissions éventuellement le changer.

Ils ont fait valoir qu'il serait préférable d'avoir un opt-in explicite pour ce type de syntaxe, en particulier parce qu'il existe d'autres cas (par exemple, let x: impl Trait = if { ... } else { ... } ) où l'on le voudrait, et nous ne pouvons pas nous attendre pour les gérer tous implicitement (certainement pas).

Je trouve cela assez convaincant. Avant cela, je supposais que nous aurions de toute façon une syntaxe d'opt-in ici, mais je voulais juste être sûr que nous n'avions pas fermé de portes prématurément. Après tout, expliquer quand vous devez insérer la « cale dynamique » est un peu délicat.

@nikomatsakis Juste mon opinion peut-être moins éclairée: bien que permettre à une fonction de renvoyer l'un des plusieurs types possibles au moment de l'exécution puisse être utile, je serais réticent à avoir la même syntaxe pour l'inférence de type de retour statique à un seul type, et permettant des situations où une décision d'exécution est requise en interne (ce que vous venez d'appeler la "cale dynamique").

Ce premier exemple de foo , pour autant que j'aie compris le problème, pourrait soit résoudre (1) un Iterator<Item = u32> encadré + type effacé, soit (2) un type de somme de std::option::Iter ou std::slice::Iter , qui à son tour dériverait une implémentation Iterator . J'essaie de faire court, car il y a eu quelques mises à jour sur la discussion (à savoir, je lis les journaux IRC maintenant) et ça devient de plus en plus difficile à comprendre : je serais certainement d'accord avec une syntaxe de type dyn pour le shim dynamique, bien que j'aie aussi comprendre que l'appeler dyn n'est peut-être pas idéal.

Plug sans vergogne et une petite note juste pour l'enregistrement : Vous pouvez obtenir facilement des types de sommes et des produits « anonymes » avec :

@Centril Ouais, ce truc de frunk est super cool. Cependant, notez que pour que CoprodInjector::inject fonctionne, le type résultant doit être inférable, ce qui est généralement impossible sans nommer le type résultant (par exemple -> Coprod!(A, B, C) ). C'est souvent le cas que vous travaillez avec des types innommables, vous auriez donc besoin de -> Coprod!(impl Trait, impl Trait, impl Trait) , ce qui échouera à l'inférence car il ne sait pas quelle variante doit contenir quel type impl Trait .

@cramertj Très vrai (note : chaque "variante" peut ne pas être entièrement innommable, mais seulement partiellement, par exemple : Map<Namable, Unnameable> ).

L'idée enum impl Trait a déjà été discutée dans https://internals.rust-lang.org/t/pre-rfc-anonymous-enums/5695

@Centril Oui, c'est vrai. Je pense spécifiquement aux futurs, où j'écris souvent des choses comme

fn foo(x: Foo) -> impl Future<Item = (), Error = Never> {
    match x {
        Foo::Bar => do_request().and_then(|res| ...).left().left(),
        Foo::Baz => do_other_thing().and_then(|res| ...).left().right(),
        Foo::Boo => do_third_thing().and_then(|res| ...).right(),
    }
}

@cramertj Je ne dirais pas que l'énumération anonyme est similaire à enum impl Trait , car nous ne pouvons pas conclure X: Tr && Y: Tr(X|Y): Tr (contre-exemple : Default trait). Les auteurs de bibliothèques devront donc manuellement impl Future for (X|Y|Z|...) .

@kennytm Vraisemblablement, nous voudrions générer automatiquement des impls de traits pour les énumérations anonymes, il semble donc que cela ressemble fondamentalement à la même fonctionnalité.

@cramertj Puisqu'un enum anonyme peut être nommé (heh), si un Default impl est généré pour (i32|String) , nous pourrions écrire <(i32|String)>::default() . OTOH <enum impl Default>::default() ne compilera tout simplement pas, donc peu importe ce que nous générons automatiquement, ce serait toujours sûr car il ne peut pas du tout être invoqué.

Néanmoins, dans certains cas, la génération automatique peut toujours poser problème avec enum impl Trait . Envisager

pub trait Rng {
    fn next_u32(&mut self) -> u32;
    fn gen<T: Rand>(&mut self) -> T where Self: Sized;
    fn gen_iter<'a, T: Rand>(&'a mut self) -> Generator<'a, T, Self> where Self: Sized;
}

Il est parfaitement normal que, si nous avons un mut rng: (XorShiftRng|IsaacRng) nous puissions calculer rng.next_u32() ou rng.gen::<u64>() . Cependant, rng.gen_iter::<u16>() ne peut pas être construit car la génération automatique ne peut produire que (Generator<'a, u16, XorShiftRng>|Generator<'a, u16, IsaacRng>) , alors que ce que nous voulons réellement est Generator<'a, u16, (XorShiftRng|IsaacRng)> .

(Peut-être que le compilateur peut rejeter automatiquement un appel de délégation non sécurisé, tout comme le contrôle Sized .)

FWIW, cette fonctionnalité me semble plus proche dans l'esprit des fermetures que des tuples (qui sont, bien sûr, la contrepartie anonyme de struct aux hypothétiques enum s anonymes). La manière dont ces choses sont « anonymes » est différente.

Pour les struct anonymes et les enum s (tuples et "disjoints"), le "anonyme" est dans le sens des types "structurels" (par opposition aux types "nominaux") -- ils' sont intégrés, entièrement génériques sur leurs types de composants et ne sont pas une déclaration nommée dans un fichier source. Mais le programmeur les écrit toujours et les utilise comme n'importe quel autre type, les implémentations de traits pour eux sont écrites explicitement comme d'habitude, et ils ne sont pas particulièrement magiques (à part avoir une syntaxe intégrée et être "variadiques", quels autres types ne peut pas encore l'être). Dans un certain sens, ils ont un nom, mais au lieu d'être alphanumériques, leur 'nom' est la syntaxe utilisée pour les écrire (parenthèses et virgules).

Les fermetures, en revanche, sont anonymes dans le sens où leur nom est secret . Le compilateur génère un nouveau type avec un nouveau nom à chaque fois que vous en écrivez un, et il n'y a aucun moyen de savoir quel est ce nom ou de vous y référer même si vous le vouliez. Le compilateur implémente un trait ou deux pour ce type de secret, et la seule façon d'interagir avec lui est à travers ces traits.

Pouvoir renvoyer différents types à partir de différentes branches d'un if , derrière un impl Trait , semble plus proche de ce dernier -- le compilateur génère implicitement un type pour contenir les différentes branches, implémente le trait demandé dessus pour envoyer à celui qui convient, et le programmeur n'écrit jamais ou ne voit jamais ce qu'est ce type, et ne peut pas s'y référer ni n'a aucune raison réelle de le vouloir.

(En fait, cette fonctionnalité semble en quelque sorte liée aux "littéraux d'objets" hypothétiques - qui seraient pour d'autres traits ce que la syntaxe de fermeture existante est pour Fn . Autrement dit, au lieu d'une seule expression lambda, vous 'implémenterait chaque méthode du trait donné (avec self étant implicite) en utilisant les variables dans la portée, et le compilateur générerait un type anonyme pour contenir les upvars, et implémenterait le trait donné pour cela, il avoir un mode optionnel move de la même manière, et ainsi de suite. Quoi qu'il en soit, je soupçonne qu'une façon différente d'exprimer if foo() { (some future) } else { (other future) } serait object Future { fn poll() { if foo() { (some future).poll() } else { (other future).poll() } } } (enfin, vous auriez aussi besoin pour déplacer le résultat de foo() dans un let afin qu'il ne soit exécuté qu'une seule fois). C'est plutôt moins ergonomique et ne devrait probablement pas être considéré comme une véritable * alternative à l'autre fonctionnalité, mais cela suggère qu'il y a une relation. Peut-être que le premier pourrait se transformer en ce dernier, ou quelque chose du genre.)

@glaebhoerl c'est une idée très intéressante ! Il y a aussi de l'art antérieur de Java ici.

Quelques pensées du haut de ma tête (donc pas très cuites):

  1. [bikeshed] le préfixe object suggère qu'il s'agit d'un objet trait plutôt que simplement existentiel -- mais ce n'est pas le cas.

Une syntaxe alternative possible :

impl Future { fn poll() { if foo() { a.poll() } else { b.poll() } } }
// ^ --
// this conflicts with inherent impls for types, so you have to delay
// things until you know whether `Future` is a type or a trait.
// This might be __very__ problematic.

// and perhaps (but probably not...):
dyn Future { fn poll() { if foo() { a.poll() } else { b.poll() } } }
  1. [macros/sucre] vous pouvez fournir du sucre syntaxique trivial afin d'obtenir :
future!(if foo() { a.poll() } else { b.poll() })

Oui, la question de syntaxe est un gâchis car il n'est pas clair si vous voulez vous inspirer de struct littéraux, fermetures ou blocs impl :) Je viens d'en choisir un du haut de ma tête, par exemple Saké. (De toute façon, mon point principal n'était pas que nous devions ajouter des littéraux d'objet [bien que nous devrions] mais que je pense que les enum anonymes sont un hareng rouge ici [bien que nous devrions les ajouter aussi].)

Être capable de renvoyer différents types à partir de différentes branches d'un if, derrière un trait impl, semble plus proche de ce dernier - le compilateur génère implicitement un type pour contenir les différentes branches, implémente le trait demandé dessus pour l'envoyer à celui qui convient, et le programmeur n'écrit jamais ou ne voit jamais ce qu'est ce type, et ne peut pas s'y référer ni n'a aucune raison réelle de le vouloir.

Hmm. J'avais donc supposé qu'au lieu de générer de "nouveaux noms" pour les types enum, nous utiliserions plutôt les types | , correspondant à des impls comme ceci :

impl<A: IntoIterator, B: IntoIterator> IntoIterator for (A|B)  { /* dispatch appropriately */ }

Il y aurait évidemment des problèmes de cohérence avec cela, dans le sens où plusieurs fonctions généreront des impls identiques. Mais même en laissant cela de côté, je me rends compte maintenant que cette idée peut ne pas fonctionner pour d'autres raisons - par exemple, s'il existe plusieurs types associés, dans certains contextes, ils peuvent être identiques, mais dans d'autres, ils peuvent être différents. Par exemple peut-être retournons-nous :

-> impl IntoIterator<Item = Y>

mais ailleurs nous le faisons

-> impl IntoIterator<IntoIter = X, Item = Y>

Il s'agirait de deux impls qui se chevauchent, je suppose qu'ils ne peuvent pas être « unis » ; bien, peut-être avec une spécialisation.

Quoi qu'il en soit, la notion d'"énumérations secrètes" semble plus propre, je suppose.

Je voudrais enregistrer une dernière préoccupation avec le trait d'impl existentiel. Une fraction substantielle des fois où je souhaite utiliser le trait impl, je souhaite en fait renvoyer plusieurs types.

@nikomatsakis : Est-il juste de dire que dans ce cas, ce qui est renvoyé est plus proche d'un dyn Trait que de impl Trait , car la valeur de retour synthétique/anonyme implémente quelque chose qui s'apparente à une répartition dynamique ?

cc https://github.com/rust-lang/rust/issues/49288 , un problème que j'ai beaucoup rencontré ces derniers temps en travaillant avec les méthodes Future s et Future -returning trait .

Comme il s'agit de la dernière chance avant la fermeture de FCP, j'aimerais faire un dernier argument contre les traits automatiques automatiques. Je me rends compte que c'est un peu à la dernière minute, donc j'aimerais tout au plus aborder formellement ce problème avant de nous engager dans la mise en œuvre actuelle.

Pour clarifier pour tous ceux qui n'ont pas suivi impl Trait , c'est le problème que je présente. Un type représenté par des types impl X implémente actuellement automatiquement les traits automatiques si et seulement si le type concret derrière eux implémente lesdits traits automatiques. Concrètement, si le changement de code suivant est effectué, la fonction continuera à se compiler, mais toute utilisation de la fonction reposant sur le fait que le type qu'elle renvoie implémente Send échouera.

 fn does_some_operation() -> impl Future<Item=(), Error=()> {
-    let data_stored = Arc::new("hello");
+    let data_stored = Rc::new("hello");

     return some_long_operation.and_then(|other_stuff| {
         do_other_calculation_with(data_stored)
     });
}

(exemple plus simple : travailler , les changements internes provoquent un échec )

Cette question n'est pas tranchée. Il y avait une décision très délibérée d'avoir des "fuites" de traits automatiques : si nous ne le faisions pas, nous devions mettre + !Send + !Sync sur chaque fonction qui renvoie quelque chose de non-Send ou non-Sync, et nous avoir une histoire peu claire avec d'autres traits automatiques personnalisés potentiels qui pourraient tout simplement ne pas être implémentables sur le type concret que la fonction renvoie. Ce sont deux problèmes que j'aborderai plus tard.

Tout d'abord, je voudrais simplement exprimer mon objection au problème : cela permet de modifier le corps d'une fonction pour changer l'API publique. Cela réduit directement la maintenabilité du code.

Tout au long du développement de la rouille, des décisions ont été prises qui privilégient la verbosité plutôt que la convivialité. Lorsque les nouveaux arrivants les voient, ils pensent que c'est de la verbosité pour la verbosité, mais ce n'est pas le cas. Chaque décision, qu'il s'agisse de ne pas avoir de structures implémentant automatiquement la copie, ou d'avoir tous les types explicites au niveau des signatures de fonction, est prise dans un souci de maintenabilité.

Lorsque je présente Rust aux gens, bien sûr, je peux leur montrer la vitesse, la productivité, la sécurité de la mémoire. Mais aller a de la vitesse. Ada a la sécurité de la mémoire. Python a de la productivité. Ce que Rust a l'emporte sur tout cela, il a la maintenabilité. Lorsqu'un auteur de bibliothèque souhaite modifier un algorithme pour qu'il soit plus efficace, ou lorsqu'il souhaite refaire la structure d'un crate, il a une forte garantie de la part du compilateur qu'il le dira en cas d'erreur. En rouille, je peux être assuré que mon code continuera à fonctionner non seulement en termes de sécurité de la mémoire, mais aussi de logique et d'interface. _Chaque interface de fonction dans Rust est entièrement représentable par la déclaration de type de la fonction_.

Stabiliser impl Trait tel quel a de grandes chances d'aller à l'encontre de cette croyance. Bien sûr, c'est extrêmement agréable pour écrire du code rapidement, mais si je veux prototyper, j'utiliserai python. Rust est le langage de choix lorsqu'on a besoin d'une maintenabilité à long terme, et non d'un code en écriture seule à court terme.


Je dis qu'il n'y a qu'une "grande chance" que cela soit mauvais ici, car encore une fois, le problème n'est pas clair. L'idée de « traits automatiques » en premier lieu n'est pas explicite. Send et Sync sont implémentés en fonction du contenu d'une structure, et non de la déclaration publique. Étant donné que cette décision a fonctionné pour la rouille, impl Trait agissant de la même manière pourrait également bien fonctionner.

Cependant, les fonctions et les structures sont utilisées différemment dans une base de code, et ce ne sont pas les mêmes problèmes.

Lorsqu'on modifie les champs d'une structure, même des champs privés, on comprend immédiatement qu'on en change le contenu réel. Les structures avec des champs non-Send ou non-Sync ont fait ce choix, et les responsables de la bibliothèque savent qu'ils doivent vérifier lorsqu'un PR modifie les champs d'une structure.

Lors de la modification des éléments internes d'une fonction, il est clair que cela peut affecter à la fois les performances et l'exactitude. Cependant, dans Rust, nous n'avons pas besoin de vérifier que nous renvoyons le bon type. Les déclarations de fonction sont un contrat difficile que nous devons respecter, et rustc veille sur nos arrières. C'est une ligne mince entre les traits automatiques sur les structures et dans les retours de fonction, mais changer les éléments internes d'une fonction est beaucoup plus routinier. Une fois que nous aurons des Future entièrement alimentés par un générateur, il sera encore plus courant de modifier les fonctions renvoyant -> impl Future . Ce seront tous des changements que les auteurs doivent rechercher pour les implémentations Send/Sync modifiées si le compilateur ne les détecte pas.

Pour résoudre ce problème, nous pourrions décider qu'il s'agit d'une charge de maintenance acceptable, comme l'a fait la discussion RFC originale . Cette section de la RFC sur le

J'ai déjà exposé ma réponse principale à cela, mais voici une dernière note. Changer la disposition d'une structure n'est pas si courant ; on peut s'en prémunir. La charge de maintenance pour s'assurer que les fonctions continuent à implémenter les mêmes traits automatiques est plus importante que celle des structures simplement parce que les fonctions changent beaucoup plus.


Pour terminer, je voudrais dire que les traits automatiques automatiques ne sont pas la seule option. C'est l'option que nous avons choisie, mais l'alternative des traits automatiques opt-out est toujours une alternative.

Nous pourrions exiger que les fonctions renvoyant des éléments non-Send / non-Sync soient à l'état + !Send + !Sync ou renvoient un trait (alias éventuellement ?) qui a ces limites. Ce ne serait pas une bonne décision, mais elle pourrait être meilleure que celle que nous choisissons actuellement.

En ce qui concerne la préoccupation concernant les traits automatiques personnalisés, je dirais que tout nouveau trait automatique ne devrait pas être mis en œuvre uniquement pour les nouveaux types introduits après le trait automatique. Cela pourrait poser plus de problèmes que je ne peux en traiter maintenant, mais ce n'est pas un problème que nous ne pouvons pas résoudre avec plus de conception.


C'est très tard et très long, et je suis certain d'avoir déjà soulevé ces objections. Je suis content de pouvoir commenter une dernière fois et de m'assurer que nous sommes entièrement d'accord avec la décision que nous prenons.

Merci d'avoir lu, et j'espère que la décision finale mettra Rust dans la meilleure direction possible.

Développer la critique de

trait FutureNSS<T, E> = Future<Item = T, Error= E> + !Send + !Sync;

fn does_some_operation() -> impl FutureNSS<(), ()> {
     let data_stored = Rc::new("hello");
     some_long_operation.and_then(|other_stuff| {
         do_other_calculation_with(data_stored)
     });
}

Ce n'est pas si mal -- vous devriez trouver un bon nom (ce que FutureNSS n'est pas). Le principal avantage est qu'il réduit la coupe de papier occasionnée par la répétition des limites.

Ne serait-il pas possible de stabiliser cette fonctionnalité avec les exigences d'énoncer explicitement les auto-traits et plus tard peut-être de supprimer ces exigences une fois que nous avons trouvé une solution appropriée à ce problème de maintenance ou une fois que nous sommes suffisamment certains qu'il n'y a en fait aucune charge de maintenance en la décision de lever les exigences?

Qu'en est-il d'exiger Send moins qu'il ne soit marqué comme !Send , mais de ne pas fournir Sync moins qu'il ne soit marqué comme Sync ? L'envoi n'est-il pas censé être plus courant que la synchronisation ?

Comme ça:

fn provides_send_only1() -> impl Trait {  compatible_with_Send_and_Sync }
fn provides_send_only2() -> impl Trait {  compatible_with_Send_only }
fn fails_to_complile1() -> impl Trait {  not_compatible_with_Send }
fn provides_nothing1() -> !Send + impl Trait { compatible_with_Send}
fn provides_nothing2() -> !Send + impl Trait { not_compatible_with_Send }
fn provides_send_and_sync() -> Sync + impl Trait {  compatible_with_Send_and_Sync }
fn fails_to_compile2() -> Sync + impl Trait { compatible_with_Send_only }

Y a-t-il une incohérence entre impl Trait dans la position d'argument et la position de retour par rapport à. traits automatiques?

fn foo(x: impl ImportantTrait) {
    // Can't use Send cause we have not required it...
}

Cela a du sens pour la position de l'argument car si vous étiez autorisé à assumer Send here, vous auriez des erreurs de post-monomorphisation. Bien sûr, les règles pour la position de retour et la position d'argument ne doivent pas nécessairement coïncider ici, mais cela pose un problème en termes d'apprentissage.

En ce qui concerne la préoccupation concernant les traits automatiques personnalisés, je dirais que tout nouveau trait automatique ne devrait pas être mis en œuvre uniquement pour les nouveaux types introduits après le trait automatique.

Eh bien, cela est vrai pour le prochain trait automatique Unpin (uniquement non implémenté pour les générateurs auto-référentiels), mais... cela semble être une chance stupide ? Est-ce une limitation avec laquelle nous pouvons vraiment vivre ? Je ne peux pas croire qu'il n'y aura pas quelque chose à l'avenir qui devra être désactivé pour par exemple &mut ou Rc ...

Je crois que cela a été discuté, et c'est bien sûr très tard, mais je ne suis toujours pas satisfait de impl Trait en position d'argument.

Les capacités à à la fois a) travailler avec des fermetures/futurs par valeur, et b) traiter certains types comme des "sorties" et donc des détails d'implémentation, sont idiomatiques et l'ont été depuis avant 1.0, car elles soutiennent directement les valeurs fondamentales de Rust de performance, stabilité, et la sécurité.

-> impl Trait fait donc que remplir une promesse faite par 1.0, ou supprimer un cas limite, ou généraliser des fonctionnalités existantes : il ajoute des types de sortie aux fonctions, en prenant le même mécanisme qui a toujours été utilisé pour gérer les types anonymes et en l'appliquant dans plus de cas. Il était peut-être plus logique de commencer avec abstract type , c'est-à-dire les types de sortie pour les modules, mais étant donné que Rust n'a pas de système de module ML, l'ordre n'est pas un gros problème.

fn f(t: impl Trait) plutôt l'impression qu'il a été ajouté « juste parce que nous le pouvons », rendant le langage plus gros et plus étrange sans donner assez en retour. J'ai lutté et je n'ai pas réussi à trouver un cadre existant pour l'intégrer. Je comprends l'argument autour de la concision de fn f(f: impl Fn(...) -> ...) , et la justification des limites peut déjà être dans les clauses <T: Trait> et where , mais celles-ci semblent creuses. Ils n'annulent pas les inconvénients :

  • Vous devez maintenant apprendre deux syntaxes pour les limites - au moins <> / where partagent une seule syntaxe.

    • Cela crée également une falaise d'apprentissage et obscurcit l'idée d'utiliser le même type générique à plusieurs endroits.

    • La nouvelle syntaxe rend plus difficile la définition d'une fonction générique. Vous devez parcourir toute la liste d'arguments.

  • Maintenant, ce qui devrait être le détail d'implémentation d'une fonction (comment elle déclare ses paramètres de type) devient une partie de son interface, car vous ne pouvez pas écrire son type !

    • Cela est également lié aux complications du trait automatique actuellement en cours de discussion - une confusion supplémentaire sur ce qui est l'interface publique d'une fonction et ce qui ne l'est pas.

  • L'analogie avec dyn Trait est, honnêtement, fausse :

    • dyn Trait signifie toujours la même chose et n'infecte pas les déclarations environnantes autrement que par le mécanisme de trait automatique existant.

    • dyn Trait est utilisable dans les structures de données, et c'est vraiment l'un de ses principaux cas d'utilisation. impl Trait dans les structures de données n'a aucun sens sans examiner toutes les utilisations de la structure de données.

    • Une partie de ce que signifie dyn Trait est l'effacement de type, mais impl Trait n'implique rien sur son implémentation.

    • Le point précédent sera encore plus confus si nous introduisons des génériques non monomorphisés. En fait, dans une telle situation, fn f(t: impl Trait) a) ne fonctionnera probablement pas avec la nouvelle fonctionnalité, et/ou b) nécessitera encore plus de conseils juridiques comme le problème des traits automatiques. Imaginez fn f<dyn T: Trait>(t: T, u: dyn impl Urait) ! :pousser un cri:

Donc, ce qui revient pour moi, c'est que impl Trait en position d'argument ajoute des cas limites, utilise davantage le budget d'étrangeté, rend le langage plus gros, etc. tandis que impl Trait en position de retour unifie, simplifie et rend la langue plus étroitement liée.

Qu'en est-il d'exiger Send à moins qu'il ne soit marqué comme !Send, mais de ne pas fournir Sync à moins qu'il ne soit marqué comme Sync ? L'envoi n'est-il pas censé être plus courant que la synchronisation ?

Cela semble très… arbitraire et ad-hoc. C'est peut-être moins de dactylographie, mais plus de mémoire et plus de chaos.

Idée de hangar à vélos ici pour ne pas détourner l'attention de mes points ci-dessus : au lieu de impl , utilisez type ? C'est le mot-clé utilisé pour les types associés, c'est probablement (l'un des) le(s) mot-clé(s) utilisé(s) pour abstract type , c'est quand même assez naturel, et cela fait davantage allusion à l'idée de "types de sortie pour les fonctions":

// keeping the same basic structure, just replacing the keyword:
fn f() -> type Trait

// trying to lean further into the concept:
fn f() -> type R: Trait
fn f() -> type R where R: Trait
fn f() -> (i32, type R) where R: Trait
// or perhaps:
fn f() -> type R: Trait in R
// or maybe just:
fn f() -> type: Trait

Merci d'avoir lu, et j'espère que la décision finale mettra Rust dans la meilleure direction possible.

J'apprécie l'objection bien écrite. Comme vous l'avez souligné, les traits automatiques ont toujours été un choix délibéré pour « exposer » certains détails de mise en œuvre dont on aurait pu s'attendre à ce qu'ils restent cachés. Je pense que - jusqu'à présent - ce choix a plutôt bien fonctionné, mais j'avoue que je suis constamment nerveux à ce sujet.

Il me semble que la question importante est de savoir dans quelle mesure les fonctions sont vraiment différentes des structs :

Changer la disposition d'une structure n'est pas si courant ; on peut s'en prémunir. La charge de maintenance pour s'assurer que les fonctions continuent à implémenter les mêmes traits automatiques est plus importante que celle des structures simplement parce que les fonctions changent beaucoup plus.

Il est vraiment difficile de savoir à quel point cela sera vrai. Il semble que la règle générale soit que l'introduction de Rc soit quelque chose à faire avec prudence - ce n'est pas tant une question de savoir vous le stockez. (En fait, le cas sur lequel je travaille vraiment n'est pas Rc mais plutôt l'introduction de dyn Trait , car cela peut être moins évident.)

Je soupçonne fortement que dans le code qui renvoie des futures, travailler avec des types non thread-safe et ainsi de suite sera rare. Vous aurez tendance à éviter ce genre de bibliothèques. (De plus, bien sûr, il est toujours avantageux d'avoir des tests exerçant votre code dans des scénarios réalistes.)

En tout cas, c'est frustrant car c'est le genre de chose qu'il est difficile de savoir à l'avance, quelle que soit la durée d'une période de stabilisation qu'on lui accorde.

Pour terminer, je voudrais dire que les traits automatiques automatiques ne sont pas la seule option. C'est l'option que nous avons choisie, mais l'alternative des traits automatiques opt-out est toujours une alternative.

C'est vrai, même si je me sens vraiment nerveux à l'idée de « distinguer » des caractéristiques automobiles spécifiques comme Send . Il faut également garder à l'esprit qu'il existe d'autres cas d'utilisation du trait impl en plus des futurs. Par exemple, renvoyer des itérateurs ou des fermetures -- et dans ces cas, il n'est pas évident que vous souhaitiez envoyer ou synchroniser par défaut. Dans tous les cas, ce que vous voudriez vraiment , et ce que nous essayons de différer =), est une sorte de borne "conditionnelle" (Send si T est Send). C'est précisément ce que les traits automatiques vous donnent.

@rpjohnst

je crois que cela a été discuté

En effet, il l'a :) depuis le premier impl Trait RFC il y a de nombreuses années. (Woah, 2014. Je me sens vieux.)

J'ai lutté et je n'ai pas réussi à trouver un cadre existant pour l'intégrer.

Je ressens tout le contraire. Pour moi, sans impl Trait en position d'argument, impl Trait en position de retour ressort d'autant plus. Le fil conducteur que je vois est :

  • impl Trait -- là où il apparaît, il indique qu'il y aura "un type monomorphisé qui implémente Trait ". (La question de savoir qui spécifie ce type - l'appelant ou l'appelé - dépend de l'endroit où le impl Trait apparaît.)
  • dyn Trait -- là où il apparaît, cela indique qu'il y aura un type qui implémentera Trait , mais que le choix du type est fait dynamiquement.

Il est également prévu d'étendre l'ensemble des endroits où impl Trait peuvent apparaître, en s'appuyant sur cette intuition. Par exemple, https://github.com/rust-lang/rfcs/pull/2071 autorise

let x: impl Trait = ...;

Le même principe s'applique : le choix du type est connu statiquement. De même, le même RFC introduit abstract type (pour lequel impl Trait peut être compris comme une sorte de sucre de syntaxe), qui peut apparaître dans les traits impls et même en tant que membres dans les modules.

Idée de hangar à vélos ici pour ne pas détourner l'attention de mes points ci-dessus : au lieu de impl , utilisez type ?

Personnellement, je ne suis pas enclin à relancer un bikeshed ici. Nous avons passé pas mal de temps à discuter de la syntaxe sur https://github.com/rust-lang/rfcs/pull/2071 et ailleurs. Il ne semble pas y avoir de "mot clé parfait", mais lire impl comme "un type qui implémente" fonctionne assez bien imo.

Permettez-moi d'ajouter un peu plus sur les fuites de traits automatiques :

Tout d'abord, en fin de compte, je pense que la fuite de traits automatiques est en fait la bonne chose à faire ici, précisément parce qu'elle est cohérente avec le reste du langage. Les traits automatiques étaient - comme je l'ai dit plus tôt - toujours un pari, mais ils semblent en avoir été un qui a essentiellement porté ses fruits. Je ne vois tout simplement pas impl Trait être si différent.

Mais aussi, je suis assez nerveux à l'idée de retarder ici. Je suis d'accord qu'il y a d'autres points intéressants dans l'espace de conception et je ne suis pas sûr à 100% que nous ayons atteint le bon endroit, mais je ne sais pas si nous en serons jamais sûrs. Je suis assez inquiet si nous tardons maintenant, nous aurons du mal à tenir notre feuille de route pour l'année.

Enfin, considérons les implications si je me trompe : ce dont nous parlons essentiellement ici, c'est que semver devient encore plus subtil à juger. C'est une préoccupation, je pense, mais qui peut être atténuée de diverses manières. Par exemple, nous pouvons utiliser des lints qui avertissent lorsque les types !Send ou !Sync sont introduits. Nous avons longtemps parlé de l'introduction d'un vérificateur de semver qui vous aide à empêcher les violations accidentelles de semver - cela semble être un autre cas où cela aiderait. Bref, un problème, mais je ne pense pas qu'il soit critique.

Donc - au moins à partir de ce moment - je me sens toujours enclin à continuer sur la voie actuelle.

Personnellement, je ne suis pas enclin à relancer un bikeshed ici.

Je n'y suis pas non plus très investi ; c'était une réflexion après coup basée sur mon impression que impl Trait en position d'argument semble être motivé par le "remplissage des trous" syntaxiquement plutôt que sémantiquement , ce qui semble être correct compte tenu de votre réponse. :)

Pour moi, sans impl Trait en position d'argument, impl Trait en position de retour ressort d'autant plus.

Compte tenu de l'analogie avec les types associés, cela ressemble beaucoup à "sans type T en position d'argument, les types associés ressortent d'autant plus". Je soupçonne que cette objection particulière n'a pas été soulevée parce que la syntaxe que nous avons choisie donne l'impression que cela n'a pas de sens - la syntaxe existante est suffisamment bonne pour que personne ne ressente le besoin de sucre syntaxique comme trait Trait<type SomeAssociatedType> .

Nous avons déjà une syntaxe pour "un type monomorphisé qui implémente Trait ." Dans le cas des traits, nous avons à la fois des variantes spécifiées par « appelant » et « appelée ». Dans le cas des fonctions, nous n'avons que la variante spécifiée par l'appelant, nous avons donc besoin de la nouvelle syntaxe pour la variante spécifiée par l'appelé.

L'extension de cette nouvelle syntaxe aux variables locales peut être justifiée, car il s'agit également d'une situation similaire à un type associé - c'est un moyen de masquer + de nommer le type de sortie d'une expression et est utile pour transférer les types de sortie des fonctions appelées.

Comme je l'ai mentionné dans mon commentaire précédent, je suis aussi un fan de abstract type . Il s'agit, encore une fois, d'une simple extension du concept de "type de sortie" aux modules. Et appliquer l'utilisation de l'inférence -> impl Trait , let x: impl Trait et abstract type aux types associés de trait impls est également très bien.

C'est précisément le concept d'ajout de cette nouvelle syntaxe pour les arguments de fonction que je n'aime pas. Il ne fait pas la même chose que les autres fonctionnalités avec lesquelles il est intégré. Il fait la même chose que la syntaxe que nous avons déjà, juste avec plus de cas limites et moins d'applicabilité. :/

@nikomatsakis

Il est vraiment difficile de savoir à quel point cela sera vrai.

Il me semble que nous devrions pécher par excès d'être conservateur alors? Pouvons-nous gagner plus de confiance dans la conception avec plus de temps (en laissant la fuite de trait automatique sous une porte de fonctionnalité distincte et uniquement la nuit pendant que nous stabilisons le reste de impl Trait ) ? Nous pouvons toujours ajouter la prise en charge des fuites de traits automatiques plus tard si nous ne le faisons pas maintenant.

Mais aussi, je suis assez nerveux à l'idée de retarder ici. [..] Je suis assez inquiet si nous tardons maintenant, nous aurons du mal à livrer notre feuille de route pour l'année.

Compréhensible! Cependant, et comme vous l'avez certainement pensé, les décisions ici nous accompagneront pendant de nombreuses années à venir.

Par exemple, nous pouvons utiliser des lints qui avertissent lorsque les types !Send ou !Sync sont introduits. Nous avons longtemps parlé de l'introduction d'un vérificateur de semver qui vous aide à empêcher les violations accidentelles de semver - cela semble être un autre cas où cela aiderait. Bref, un problème, mais je ne pense pas qu'il soit critique.

C'est bon à entendre ! Et je pense que cela apaise surtout mes inquiétudes.

C'est vrai, même si je me sens vraiment nerveux à l'idée de « distinguer » des caractéristiques automobiles spécifiques comme Send .

Je suis tout à fait d'accord avec ce sentiment 👍.

Dans tous les cas, ce que vous voudriez vraiment, et ce que nous essayons de différer =), est une sorte de borne "conditionnelle" (Send si T est Send). C'est précisément ce que les traits automatiques vous donnent.

Je pense que T: Send => Foo<T>: Send serait mieux compris si le code l'indiquait explicitement.

fn foo<T: Extra, trait Extra = Send>(x: T) -> impl Bar + Extra {..}

Comme nous en avons discuté dans WG-Traits, vous n'aurez peut-être pas du tout d'inférence ici, vous devez donc toujours spécifier Extra si vous voulez autre chose que Send , ce qui serait une déception totale .

@rpjohnst

L'analogie avec dyn Trait est, honnêtement, fausse :

En ce qui concerne impl Trait en position d'argument, il est faux, mais pas avec -> impl Trait car les deux sont des types existentiels.

  • Maintenant, ce qui devrait être le détail d'implémentation d'une fonction (comment elle déclare ses paramètres de type) devient une partie de son interface, car vous ne pouvez pas écrire son type !

Je voudrais noter que l'ordre des paramètres de type n'a jamais été un détail d'implémentation en raison de turbofish, et à cet égard, je pense que impl Trait peut aider car il vous permet de laisser certains arguments de type non spécifiés dans turbofish .

[..] la syntaxe existante est suffisamment bonne pour que personne ne ressente le besoin de sucre syntaxique comme trait Trait.

Ne jamais dire jamais? https://github.com/rust-lang/rfcs/issues/2274

Comme @nikomatsakis , j'apprécie beaucoup le soin apporté à ces commentaires de dernière minute ; Je sais que cela peut donner l'impression d'essayer de se jeter devant un train de marchandises, surtout pour une fonctionnalité aussi longtemps désirée que celle-ci !


@daboross , je voulais

Malheureusement, cependant, il se heurte à certains problèmes une fois que vous commencez à regarder la situation dans son ensemble :

  • Si les traits automatiques étaient traités comme une option de retrait pour impl Trait , ils devraient également l'être pour dyn Trait .
  • Ceci s'applique bien sûr même lorsque ces constructions sont utilisées en position d'argument.
  • Mais alors, il serait assez étrange que les génériques se comportent différemment. En d'autres termes, pour fn foo<T>(t: T) , vous pouvez raisonnablement vous attendre à T: Send par défaut.
  • Nous avons bien sûr un mécanisme pour cela, actuellement appliqué uniquement à Sized ; c'est un trait qui est assumé par défaut partout, et pour lequel vous vous désinscrivez en écrivant ?Sized

Le mécanisme ?Sized reste l'un des aspects les plus obscurs et les plus difficiles à enseigner de Rust, et nous avons en général été extrêmement réticents à l'étendre à d'autres concepts. L'utiliser pour un concept aussi central que Send semble risqué - sans parler, bien sûr, que ce serait un changement majeur.

De plus, cependant : nous ne voulons vraiment pas incorporer une hypothèse de trait automatique pour les génériques, car une partie de la beauté des génériques aujourd'hui est que vous pouvez effectivement être générique pour savoir si un type implémente un trait automatique, et avoir cette information juste "s'écouler à travers". Par exemple, considérons fn f<T>(t: T) -> Option<T> . Nous pouvons transmettre T qu'il s'agisse de Send , et la sortie sera Send ssi T was. C'est une partie extrêmement importante de l'histoire des génériques dans Rust.

Il y a aussi des problèmes avec dyn Trait . En particulier, en raison d'une compilation séparée, nous serions obligés de restreindre cette nature de « désabonnement » uniquement aux traits automatiques « bien connus » comme Send et Sync ; cela signifierait probablement ne jamais stabiliser auto trait pour un usage externe.

Enfin, il vaut la peine de réitérer que la conception de "fuite" a été explicitement modélisée d'après ce qui se passe aujourd'hui lorsque vous créez un wrapper newtype pour renvoyer un type opaque. Fondamentalement, je crois que la « fuite » est un aspect inhérent aux traits automatiques en premier lieu ; il y a des compromis, mais c'est ce qu'est la fonctionnalité au cœur, et je pense que nous devrions nous efforcer de créer de nouvelles fonctionnalités pour interagir avec elle en conséquence.


@rpjohnst

Je n'ai pas grand-chose à ajouter sur la question de la position de l'argument après les discussions approfondies sur le RFC et le commentaire de synthèse de @nikomatsakis ci-dessus.

Maintenant, ce qui devrait être le détail d'implémentation d'une fonction (comment elle déclare ses paramètres de type) devient une partie de son interface, car vous ne pouvez pas écrire son type !

Je ne comprends pas ce que tu veux dire par là. Pouvez-vous étendre?

Je tiens également à noter que des phrases telles que :

fn f(t: impl Trait) donne plutôt l'impression qu'il a été ajouté "juste parce que nous pouvons"

saper la discussion de bonne foi (je l'appelle parce que c'est un modèle répété). La RFC fait des efforts considérables pour motiver la fonctionnalité et réfuter certains des arguments que vous avancez ici - sans parler de la discussion sur le fil bien sûr, et dans les itérations précédentes de la RFC, etc.

Des compromis existent, il y a effectivement des inconvénients, mais cela ne nous aide pas à tirer une conclusion raisonnée de caricaturer "l'autre côté" du débat.

Merci à tous pour leurs commentaires détaillés ! Je suis vraiment super excité d'envoyer enfin impl Trait sur stable, donc je suis fortement biaisé vers l'implémentation actuelle et les décisions de conception qui y ont conduit. Cela dit, je ferai de mon mieux pour répondre le plus impartialement possible et considérer les choses comme si nous partions de zéro :

auto Trait Fuite

L'idée d' auto Trait fuite de const fn , exigeant que les fonctions se spécifient explicitement comme const plutôt que d'inférer const ness à partir des corps de fonction. Tout comme les limites de traits explicites, cela permet de déterminer plus facilement quelles fonctions sont utilisables de quelle manière et donne aux auteurs de bibliothèques l'assurance que de petits changements d'implémentation ne briseront pas les utilisateurs.

Cela dit, j'ai largement utilisé la position impl Trait retour + Send à pratiquement chaque fonction impl Trait -using que j'ai jamais écrite. Les limites négatives (nécessitant + !Send ) sont une idée intéressante pour moi, mais alors j'écrirais + !Unpin sur presque toutes les mêmes fonctions. L'explicitation est utile lorsqu'elle éclaire les décisions des utilisateurs ou rend le code plus compréhensible. Dans ce cas, je pense que cela ne ferait ni l'un ni l'autre.

Send et Sync sont des "contextes" dans lesquels les utilisateurs programment : il est extrêmement rare que j'écrive une application ou une bibliothèque qui utilise à la fois les types Send et !Send (en particulier lors de l'écriture de code async à exécuter sur un exécuteur central, qui est multithread ou non). Le choix d'être thread-safe ou non est l'un des premiers choix à faire lors de l'écriture d'une application, et à partir de là, choisir d'être thread-safe signifie que tous mes types doivent être Send . Pour les bibliothèques, c'est presque toujours le cas que je préfère les types Send , car ne pas les utiliser signifie généralement que ma bibliothèque est inutilisable (ou nécessite la création d'un thread dédié) lorsqu'elle est utilisée dans un contexte threadé. Un parking_lot::Mutex incontesté aura des performances presque identiques à RefCell lorsqu'il est utilisé sur des processeurs modernes, donc je ne vois aucune motivation pour pousser les utilisateurs à se spécialiser dans les fonctionnalités de la bibliothèque pour une utilisation !Send cas. Pour ces raisons, je ne pense pas qu'il soit important de pouvoir discerner entre les types Send et !Send au niveau fonction-signature, et je ne pense pas que ce sera banal pour auteurs de bibliothèques d'introduire accidentellement des types !Send dans des types impl Trait qui étaient auparavant Send . Il est vrai que ce choix a un coût en termes de lisibilité et de clarté, mais je pense que le compromis en vaut la peine pour les avantages en termes d'ergonomie et de convivialité.

Argument-position impl Trait

Je n'ai pas grand-chose à dire ici, sauf qu'à chaque fois que j'ai atteint la position d'argument impl Trait , j'ai constaté que cela augmentait considérablement la lisibilité et l'agrément général de mes signatures de fonction. Il est vrai qu'il n'ajoute pas une nouvelle capacité qui n'est pas possible dans Rust d'aujourd'hui, mais c'est une grande amélioration de la qualité de vie pour les signatures de fonctions compliquées, il se marie bien conceptuellement avec la position impl Trait retour F dans fn foo<F>(x: F) where F: FnOnce() vs fn foo(x: impl FnOnce()) ). Ce changement résout ce problème et se traduit par des signatures de fonctions plus faciles à lire et à écrire, et IMO semble s'intégrer naturellement aux côtés de -> impl Trait .

TL ; DR : Je pense que nos décisions initiales étaient les bonnes, même si elles s'accompagnent sans aucun doute de compromis.
J'apprécie vraiment que tout le monde s'exprime et investisse autant de temps et d'efforts pour s'assurer que Rust est le meilleur langage possible.

@Centril

En ce qui concerne impl Trait en position d'argument, il est faux, mais pas avec -> impl Trait car les deux sont des types existentiels.

Oui, c'est ce que je voulais dire.

@aturon

des expressions telles que ... sapent une discussion de bonne foi

Tu as raison, je m'en excuse. Je crois que j'ai mieux fait valoir mon point de vue ailleurs.

Maintenant, ce qui devrait être le détail d'implémentation d'une fonction (comment elle déclare ses paramètres de type) devient une partie de son interface, car vous ne pouvez pas écrire son type !

Je ne comprends pas ce que tu veux dire par là. Pouvez-vous étendre?

Avec la prise en charge de impl Trait en position d'argument, vous pouvez écrire cette fonction de deux manières :

fn f(t: impl Trait)
fn f<T: Trait>(t: T)

Le choix de la forme détermine si le consommateur de l'API peut même écrire le nom d'une instanciation particulière (par exemple pour prendre son adresse). La variante impl Trait ne vous permet pas de faire cela, et cela ne peut pas toujours être contourné sans réécrire la signature pour utiliser la syntaxe <T> . De plus, le passage à la syntaxe <T> est un changement radical !

Au risque de caricaturer davantage, la motivation en est qu'il est plus facile à enseigner, à apprendre et à utiliser. Cependant, comme le choix entre les deux est également une partie importante de l'interface de la fonction, tout comme l'ordre des paramètres de type, je ne pense pas que cela ait été correctement traité - je ne conteste pas en fait qu'il est plus facile à utiliser ou qu'il se traduit par des signatures de fonction plus agréables.

Je ne suis pas sûr qu'aucun de nos autres changements "simples, mais limités -> complexes, mais généraux", motivés par la facilité d'apprentissage/l'ergonomie, impliquent des changements d'interface de cette manière. Soit l'équivalent complexe de la méthode simple se comporte de manière identique et vous n'avez besoin de changer que lorsque vous modifiez déjà l'interface ou le comportement (par exemple, élision à vie, ergonomie des correspondances, -> impl Trait ), soit le changement est tout aussi général et destiné à être appliqué universellement (par exemple, modules/chemins, durées de vie dans la bande, dyn Trait ).

Pour être plus concret, je crains que nous ne commencions à résoudre ce problème dans les bibliothèques, et ce sera un peu comme "tout le monde doit se rappeler de dériver Copy / Clone ", mais pire parce que a) ce sera un changement décisif, et b) il y aura toujours une tension pour revenir en arrière, précisément parce que c'est pour cela que la fonctionnalité a été conçue !

@cramertj En ce qui concerne la redondance de la signature des fonctions, pourrions-nous nous en débarrasser d'une autre manière ? Les durées de vie dans la bande ont pu s'en tirer sans références arrière ; peut-être pourrions-nous faire l'équivalent moral des "paramètres de type intrabande" d'une manière ou d'une autre. Ou en d'autres termes, "le changement est tout aussi général et destiné à être appliqué universellement".

@rpjohnst

De plus, le passage à la syntaxe <T> est un changement radical !

Pas nécessairement, avec https://github.com/rust-lang/rfcs/pull/2176, vous pouvez ajouter un paramètre de type supplémentaire T: Trait à la fin et le turbofish fonctionnerait toujours (sauf si vous faites référence à la rupture par autre moyen que la casse du turbofish).

La variante impl Trait ne vous permet pas de faire cela, et cela ne peut pas toujours être contourné sans réécrire la signature pour utiliser la syntaxe <T> . De plus, passer à la syntaxe <T> est un changement radical !

De plus, je pense que vous voulez dire que passer de la syntaxe <T> est un changement radical (car les appelants ne peuvent plus spécifier la valeur de T explicitement en utilisant turbofish).

MISE À JOUR : Notez que si une fonction utilise impl Trait, nous n'autorisons actuellement pas du tout l'utilisation de turbofish, même si elle a des paramètres génériques normaux.

@nikomatsakis Le passage à la syntaxe explicite peut également être un changement décisif , si l'ancienne signature comportait un mélange de paramètres de type explicites et implicites - toute personne ayant fourni des paramètres de type n devra désormais fournir n + 1 place. C'était l'un des cas que la RFC de

MISE À JOUR : Notez que si une fonction utilise impl Trait, nous n'autorisons actuellement pas du tout l'utilisation de turbofish, même si elle a des paramètres génériques normaux.

Cela réduit techniquement le nombre de cas de rupture, mais d'un autre côté, cela augmente le nombre de cas où vous ne pouvez pas nommer une instanciation spécifique. :(

@nikomatsakis

Merci d'avoir répondu sincèrement à cette préoccupation.

J'hésite toujours à dire que la fuite de traits automatiques est la bonne solution, mais je suis d'accord pour dire que nous ne pouvons vraiment savoir ce qui est le mieux qu'après coup.

J'avais principalement envisagé le cas d'utilisation de Futures, mais ce n'est pas le seul. Sans fuite Send/Sync des types locaux, il n'y a pas vraiment de bonne histoire pour utiliser impl Trait dans de nombreux contextes différents. Compte tenu de cela, et compte tenu des caractéristiques automatiques supplémentaires, ma suggestion n'est pas vraiment viable.

Je n'avais pas voulu distinguer Sync et Send et _seulement_ les assumer, car c'est un peu arbitraire et c'est le mieux pour _un_ cas d'utilisation. Cependant, l'alternative consistant à supposer tous les traits automatiques ne serait pas bonne non plus. + !Unpin + !... sur chaque type ne semble pas être une solution viable.

Si nous avions encore cinq ans de conception de langage pour trouver un système d'effets et d'autres idées dont je n'ai aucune idée maintenant, nous pourrions peut-être trouver quelque chose de mieux. Mais pour l'instant, et pour Rust, il semble qu'avoir des traits automatiques 100% "auto" soit la meilleure voie à suivre.

@fairy

Le passage à la syntaxe explicite peut également être un changement décisif, si l'ancienne signature comportait un mélange de paramètres de type explicites et implicites -- toute personne ayant fourni des paramètres de type n devra désormais fournir n + 1 place.

Ce n'est pas permis actuellement. Si vous utilisez impl Trait , vous n'obtenez de turbofish pour aucun paramètre (comme je l'ai noté). Ceci n'est pas conçu comme une solution à long terme, mais plutôt comme une étape conservatrice pour éviter les désaccords sur la façon de procéder jusqu'à ce que nous ayons le temps de proposer une conception arrondie. (Et, comme l' a noté a ses propres inconvénients .)

La conception que j'aimerais voir est (a) oui d'accepter le RFC de @centril ou quelque chose comme ça et (b) de dire que vous pouvez utiliser turbofish pour les paramètres explicites (mais pas les types impl Trait ). Cependant, nous ne l'avons pas fait, en partie parce que nous nous demandions s'il pourrait y avoir une histoire qui permettrait la migration d' un paramètre explicite vers un trait impl.

@fairy

C'était l'un des cas que la RFC de

_[Trivia]_ D' ailleurs, c'est en fait @nikomatsakis qui <T: Trait> et impl Trait ;) Ce n'était pas un objectif du RFC à tout depuis le début, mais ce fut une belle surprise. ??

Espérons qu'une fois que nous aurons acquis plus de confiance dans l'inférence, les valeurs par défaut, les paramètres nommés, etc., nous pourrons également avoir un turbofish partiel, Eventually™.

La dernière période de commentaires est maintenant terminée.

Si cela est expédié en 1.26, alors https://github.com/rust-lang/rust/issues/49373 me semble très important, Future et Iterator sont deux des principales utilisations -cas et ils sont tous deux très dépendants de la connaissance des types associés.

J'ai effectué une recherche rapide dans le suivi des problèmes et le numéro 47715 est un ICE qui doit encore être corrigé. Pouvons-nous l'obtenir avant qu'il ne devienne stable ?

Quelque chose que j'ai rencontré avec impl Trait aujourd'hui :
https://play.rust-lang.org/?gist=69bd9ca4d41105f655db5f01ff444496&version=stable

Il semble que impl Trait soit incompatible avec unimplemented!() - est-ce un problème connu ?

oui, voir #36375 et #44923

Je viens de réaliser que l' hypothèse 2 de la RFC 1951 se heurte à certaines de mes utilisations prévues de impl Trait avec des blocs asynchrones. Plus précisément, si vous prenez un paramètre générique AsRef ou Into pour avoir une API plus ergonomique, puis le transformez en un type appartenant avant de renvoyer un bloc async , vous obtenez toujours le résultat impl Trait type étant lié par toutes les durées de vie dans ce paramètre, par exemple

impl HttpClient {
    fn get(&mut self, url: impl Into<Url>) -> impl Future<Output = Response> + '_ {
        let url = url.into();
        async {
            // perform the get
        }
    }
}

fn foo(client: &mut HttpClient) -> impl Future<Output = Response> + '_ {
    let url = Url::parse("http://foo.example.com").unwrap();
    client.get(&url)
}

Avec cela, vous obtiendrez un error[E0597]: `url` does not live long enough car get inclut la durée de vie de la référence temporaire dans le impl Future retourné. Cet exemple est légèrement artificiel en ce sens que vous pouvez passer l'url par valeur dans get , mais il y aura presque certainement des cas similaires dans le code réel.

Autant que je sache, le correctif attendu pour ce problème concerne les types abstraits, en particulier

impl HttpClient {
    abstract type Get<'a>: impl Future<Output = Response> + 'a;
    fn get(&mut self, url: impl Into<Url>) -> Self::Get<'_> {
        let url = url.into();
        async {
            // perform the get
        }
    }
}

En ajoutant la couche d'indirection, vous devez explicitement indiquer quel type générique et quels paramètres de durée de vie sont requis pour le type abstrait.

Je me demande s'il existe potentiellement un moyen plus succinct d'écrire cela, ou est-ce que cela finira par utiliser des types abstraits pour presque toutes les fonctions et jamais le simple type de retour impl Trait ?

Donc, si je comprends le commentaire de @cramertj sur ce problème, vous obtiendrez une erreur sur la définition de HttpClient::get quelque chose comme `get` returns an `impl Future` type which is bounded to live for `'_`, but this type could potentially contain data with a shorter lifetime inside the type of `url` . (Parce que la RFC spécifie explicitement que impl Trait capture _tous_ les paramètres de type générique, et c'est un bogue que vous êtes autorisé à capturer un type qui peut contenir une durée de vie plus courte que votre durée de vie explicitement déclarée).

À partir de là, le seul correctif semble toujours être de déclarer un type abstrait nominal pour permettre de déclarer explicitement quels paramètres de type sont capturés.

En fait, cela semble être un changement radical. Donc, si une erreur dans ce cas doit être ajoutée, il vaut mieux que ce soit bientôt.

EDIT: Et en relisant le commentaire, je ne pense pas que ce soit ce qu'il dit, donc je ne sais toujours pas s'il existe un moyen potentiel de contourner cela sans utiliser de types abstraits ou non.

@Nemo157 Oui, la correction de #42940 résoudrait votre problème de durée de vie puisque vous pouvez spécifier que le type de retour doit vivre aussi longtemps que l'emprunt de soi, quelle que soit la durée de vie de Url . C'est certainement un changement que nous voulons apporter, mais je pense que cela est rétrocompatible - cela ne permet pas au type de retour d'avoir une durée de vie plus courte, cela restreint trop les manières dont le type de retour peut être utilisé.

Par exemple, les erreurs suivantes avec "le paramètre Iter peuvent ne pas durer assez longtemps" :

fn foo<'a, Iter>(_: &'a mut u32, iter: Iter) -> impl Iterator<Item = u32> + 'a
    where Iter: Iterator<Item = u32>
{
    iter
}

Le simple fait d'avoir le Iter dans les génériques de la fonction n'est pas suffisant pour lui permettre d'être présent dans le type de retour, mais actuellement, les appelants de la fonction supposent à tort que c'est le cas. C'est certainement un bogue et devrait être corrigé, mais je pense qu'il peut être corrigé de manière rétrocompatible et ne devrait pas bloquer la stabilisation.

Il semble que le #46541 soit terminé. Quelqu'un peut-il mettre à jour l'OP?

Y a-t-il une raison pour laquelle la syntaxe abstract type Foo = ...; été choisie plutôt que type Foo = impl ...; ? J'ai beaucoup préféré ce dernier, pour la cohérence de la syntaxe, et je me souviens d'une discussion à ce sujet il y a quelque temps, mais je n'arrive pas à le trouver.

Je suis partisan de type Foo = impl ...; ou type Foo: ...; , abstract semble un excentrique inutile.

Si je me souviens bien, l'une des principales préoccupations était que les gens ont appris à interpréter type X = Y comme une substitution textuelle ("remplacer X par Y cas échéant"). Cela ne fonctionne pas pour type X = impl Y .

Je préfère type X = impl Y moi-même parce que mon intuition est que type fonctionne comme let , mais...

@alexreg Il y a beaucoup de discussions sur le sujet sur RFC 2071 . TL;DR : type Foo = impl Trait; brise la capacité de désagréger impl Trait dans une forme "plus explicite", et cela brise les intuitions des gens sur les alias de type fonctionnant comme une substitution syntaxique légèrement plus intelligente.

J'ai un faible pour taper Foo = impl ...; ou tapez Foo: ...;, l'abstrait semble inutile

Tu devrais rejoindre mon camp exists type Foo: Trait; :wink:

@cramertj Hum. Je viens de me rafraîchir une partie de cela, et si je suis honnête, je ne peux pas dire que je comprends le raisonnement de @withoutboats . Cela me semble à la fois le plus intuitif (avez-vous un contre-exemple ?) Je suppose que mon intuition fonctionne comme @lnicola. Je pense également que cette syntaxe est la meilleure pour faire des choses comme https://github.com/rust-lang/rfcs/pull/2071#issuecomment -319012123 - cela peut-il même être fait dans la syntaxe actuelle ?

exists type Foo: Trait; est une légère amélioration, même si j'abandonnerais toujours le mot-clé exists . type Foo: Trait; ne me dérangerait pas assez pour me plaindre. 😉 abstract est juste superflu/bizarre, comme le dit @eddyb .

@alexreg

cela peut-il même être fait dans la syntaxe actuelle?

Oui, mais c'est beaucoup plus gênant. C'était ma principale raison de préférer la syntaxe = impl Trait (modulo le mot-clé abstract ).

type Foo = (impl Bar, impl Baz);
type IterDisplay = impl Iterator<Item=impl Display>;

// can be written like this:

exists type Foo1: Bar;
exists type Foo2: Baz;
exists type Foo: (Foo1, Foo2);

exists type IterDisplayItem: Display;
exists type IterDisplay: Iterator<Item=IterDisplayItem>;

Edit : exists type Foo: (Foo1, Foo2); ci-dessus aurait dû être type Foo = (Foo1, Foo2); . Désolé pour la confusion.

@cramertj La syntaxe semble agréable. exists devrait-il être un mot-clé approprié ?

@cramertj D'accord , je pensais que tu devrais faire quelque chose comme ça... une bonne raison de préférer = impl Trait , je pense ! Honnêtement, si les gens pensent que l'intuition de la substitution s'effondre suffisamment pour les types existentiels ici (par rapport aux alias de types simples), alors pourquoi pas le compromis suivant ?

exists type Foo = (impl Bar, impl Baz);

(Honnêtement, je préférerais simplement avoir la cohérence d'utiliser le seul mot-clé type pour tout.)

Je trouve:

exists type Foo: (Foo1, Foo2);

profondément étrange. L'utilisation de Foo: (Foo1, Foo2) où le RHS n'est pas une limite n'est pas cohérente avec la façon dont Ty: Bound est utilisé ailleurs dans la langue.

Les formulaires suivants me semblent bien :

exists type Foo: Bar + Baz;  // <=> "There exists a type Foo which satisfies Bar and Baz."
                             // Reads super well!

type Foo = impl Bar + Baz;

type Bar = (impl Foo, impl Bar);

Je préfère également ne pas utiliser abstract comme mot ici.

Je trouve exists type Foo: (Foo1, Foo2); profondément étrange

Cela me semble certainement être une erreur, et je pense que cela devrait dire type Foo = (Foo1, Foo2); .

Si nous faisons du bikeshed abstract type contre exists type ici, je soutiendrais certainement le premier. Principalement parce que « abstrait » fonctionne comme un adjectif. Je pourrais facilement appeler quelque chose un "type abstrait" dans une conversation, alors qu'il est étrange de dire que nous créons un "type existant".

Je préférerais aussi toujours : Foo + Bar à : (Foo, Bar) , = Foo + Bar , = impl Foo + Bar ou = (impl Foo, impl Bar . L'utilisation de + fonctionne bien avec tous les autres endroits où les limites peuvent être, et l'absence de = signifie vraiment que nous ne pouvons pas écrire le type complet. Nous ne créons pas un alias de type ici, nous créons un nom pour quelque chose dont nous garantissons qu'il a certaines limites, mais que nous ne pouvons pas nommer explicitement.


J'aime aussi toujours la suggestion de syntaxe de https://github.com/rust-lang/rfcs/pull/2071#issuecomment -318852774 de :

type ExistentialFoo: Bar;
type Bar: Baz + Bax;

Bien que ce soit, comme mentionné dans ce fil, un peu trop peu de différence et pas très explicite.

Je dois interpréter (impl Foo, impl Bar) très différemment de certains d'entre vous... pour moi, cela signifie que le type est un 2-uplet de certains types existentiels, et est complètement différent de impl Foo + Bar .

@alexreg Si c'était l'intention de : :

exists type Foo: (Foo1, Foo2);

semble encore très peu clair quant à ce qu'il fait - les limites ne spécifient généralement pas un tuple de types possibles dans tous les cas, et cela pourrait facilement être confondu avec la signification de la syntaxe Foo: Foo1 + Foo2 .

= (impl Foo, impl Bar) est une idée intéressante - permettre de créer des tuples existentiels avec des types qui ne sont pas eux-mêmes connus serait intéressant. Je ne pense pas que nous ayons besoin de les prendre en charge, car nous pouvons simplement introduire deux types existentiels pour impl Foo et impl Bar puis un troisième alias de type pour le tuple.

@daboross Eh bien, vous "type existentiel" , pas un "type existe" ; c'est ainsi qu'on l'appelle en théorie des types. Mais je pense que l'expression "il existe un type Foo qui ..." fonctionne bien à la fois avec le modèle mental et d'un point de vue théorique de type.

Je ne pense pas que nous ayons besoin de les prendre en charge, car nous pouvons simplement introduire deux types existentiels pour impl Foo et impl Bar puis un troisième alias de type pour le tuple.

Cela semble peu ergonomique... les temporaires ne sont pas si gentils imo.

@alexreg Remarque : je ne voulais pas dire que impl Bar + Baz; est identique à (impl Foo, impl Bar) , ce dernier est évidemment le 2-uplet.

@daboross

Si c'était l'intention de

exists type Foo: (Foo1, Foo2);

semble encore très peu clair quant à ce qu'il fait - les limites ne spécifient généralement pas un tuple de types possibles dans tous les cas, et cela pourrait facilement être confondu avec la signification de la syntaxe Foo: Foo1 + Foo2.

C'est peut-être un peu flou (pas aussi explicite que (impl Foo, impl Bar) , que je comprendrais intuitivement tout de suite) – mais je ne pense pas que je le confondrais jamais avec Foo1 + Foo2 , personnellement.

= (impl Foo, impl Bar) est une idée intéressante - permettre de créer des tuples existentiels avec des types qui ne sont pas eux-mêmes connus serait intéressant. Je ne pense pas que nous ayons besoin de les prendre en charge, car nous pouvons simplement introduire deux types existentiels pour impl Foo et impl Bar, puis un troisième alias de type pour le tuple.

Oui, c'était une proposition précoce, et je l'aime toujours beaucoup. Il a été noté que cela peut être fait de toute façon avec la syntaxe actuelle, mais cela nécessite 3 lignes de code, ce qui n'est pas très ergonomique. Je maintiens également qu'une syntaxe telle que ... = (impl Foo, impl Bar) est la plus claire pour l'utilisateur, mais je sais qu'il y a un conflit ici.

@Centril Je ne le pensais pas au début, mais c'était légèrement ambigu, puis @daboross a semblé l'interpréter de cette façon, hah. Quoi qu'il en soit, content que nous ayons éclairci cela.

Oups, voir ma modification sur https://github.com/rust-lang/rust/issues/34511#issuecomment -386763340. exists type Foo: (Foo1, Foo2); aurait dû être type Foo = (Foo1, Foo2); .

@cramertj Ah, cela a plus de sens maintenant. Quoi qu'il en soit, ne pensez-vous pas que pouvoir faire ce qui suit est le plus ergonomique? Même en parcourant cet autre fil, je n'ai pas vraiment vu d'argument contre lui.

type A = impl Foo;
type B = (impl Foo, impl Bar, String);

@alexreg Oui, je pense que est la plus ergonomique syntaxe.

En utilisant RFC https://github.com/rust-lang/rfcs/pull/2289 , voici comment je réécrirais l' @cramertj :

type Foo = (impl Bar, impl Baz);
type IterDisplay = impl Iterator<Item: Display>;

// alternatively:

exists type IterDisplay: Iterator<Item: Display>;

type IterDisplay: Iterator<Item: Display>;

Cependant, je pense que pour les alias de type, ne pas introduire exists aiderait à conserver le pouvoir expressif sans rendre inutilement la syntaxe du langage plus complexe ; donc à partir d'un POV de budget de complexité, impl Iterator semble mieux que exists . La dernière alternative n'introduit cependant pas vraiment de nouvelle syntaxe et est également la plus courte tout en étant claire.

En résumé, je pense que les deux formes suivantes devraient être autorisées (car cela fonctionne à la fois sous le impl Trait et les limites sur les syntaxes de types associés que nous avons déjà):

type Foo = (impl Bar, impl Baz);
type IterDisplay: Iterator<Item: Display>;

EDIT : Quelle syntaxe faut-il utiliser ? IMO, clippy devrait sans ambiguïté préférer la syntaxe Type: Bound lorsqu'elle est possible à utiliser car elle est la plus ergonomique et la plus directe.

Je préfère de loin la variante type Foo: Trait variante type Foo = impl Trait . Il correspond à la syntaxe du type associé, ce qui est bien car c'est aussi un "type de sortie" du module qui le contient.

La syntaxe impl Trait est déjà utilisée pour les types d'entrée et de sortie, ce qui signifie qu'elle risque de donner l'impression de modules polymorphes. :(

Si impl Trait étaient uniquement utilisés pour les types de sortie, alors je préférerais peut-être la variante type Foo = impl Trait au motif que la syntaxe de type associée est davantage pour les traits (qui correspondent approximativement aux signatures ML) tandis que le type Foo = .. syntaxe

@rpjohnst

Je préfère de loin la variante type Foo: Trait variante type Foo = impl Trait .

Je suis d'accord, il devrait être utilisé chaque fois que possible; mais qu'en est-il du cas de (impl T, impl U) où la syntaxe liée ne peut pas être utilisée directement ? Il me semble que l'introduction d'alias de type temporaires nuit à la lisibilité.

Utiliser simplement type Name: Bound semble être déroutant lorsqu'il est utilisé à l'intérieur impl blocs

impl Iterator for Foo {
    type Item: Display;

    fn next(&mut self) -> Option<Self::Item> { Some(5) }
}

Pour cette syntaxe et le plan actuel (?) du préfixe de mot-clé, le coût d'introduction des alias de type temporaires à utiliser dans les blocs impl est également beaucoup plus important, ces alias de type doivent maintenant être exportés au niveau du module ( et donné un nom sémantiquement significatif...), qui bloque un modèle relativement courant (au moins pour moi) de définition des implémentations de traits à l'intérieur de modules privés.

pub abstract type First: Display;
pub abstract type Second: Debug;

impl Iterator for Foo {
    type Item = (First, Second);

    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

vs

impl Iterator for Foo {
    type Item = (impl Display, impl Debug);

    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

@ Nemo157 Pourquoi ne pas autoriser les deux :

pub type First: Display;
pub type Second: Debug;

impl Iterator for Foo {
    type Item = (First, Second);
    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

et:

impl Iterator for Foo {
    type Item = (impl Display, impl Debug);
    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

?

Je ne vois pas pourquoi il faudrait deux syntaxes pour la même fonctionnalité, il serait toujours possible d'utiliser uniquement la syntaxe type Name = impl Bound; fournissant explicitement des noms pour les deux parties :

pub type First = impl Display;
pub type Second = impl Debug;

impl Iterator for Foo {
    type Item = (First, Second);
    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

@ Nemo157 Je suis d'accord qu'il n'y a pas besoin (et ne devrait pas) être deux syntaxes différentes. Je ne trouve pas du tout type (sans mot-clé préfixe) déroutant, je dois dire.

@rpjohnst Qu'est-ce que c'est qu'un module polymorphe ? :-) Quoi qu'il en soit, je ne vois pas pourquoi nous devrions modéliser la syntaxe après les définitions de type associées, qui placent des limites de trait sur un type. Cela n'a rien à voir avec les limites .

@alexreg Un module polymorphe est un module qui a des paramètres de type, de la même manière que fn foo(x: impl Trait) . Ce n'est pas quelque chose qui existe, et donc je ne veux pas que les gens pensent que ça existe.

abstract type ( edit : pour nommer la fonctionnalité, pas pour suggérer l'utilisation du mot-clé) a tout à voir avec les limites ! Les limites sont la seule chose que vous savez sur le type. La seule différence entre eux et les types associés est qu'ils sont inférés, car ils sont généralement innommables.

@Nemo157 la Foo: Bar est déjà plus familière dans d'autres contextes (limites sur les types associés, et sur les paramètres de type) et est plus ergonomique et (IMO) claire lorsqu'elle peut être utilisée sans introduire de temporaires.

L'écriture:

type IterDisplay: Iterator<Item: Display>;

semble beaucoup plus direct wrt. ce que je veux dire, par rapport à

type IterDisplay = impl Iterator<Item = impl Display>;

Je pense qu'il s'agit simplement d'appliquer systématiquement la syntaxe que nous avons déjà ; donc ce n'est pas vraiment nouveau.

EDIT2: La première syntaxe est également la façon dont je voudrais qu'elle soit rendue dans rustdoc.

Passer d'un trait qui nécessite quelque chose sur un type associé à un impl devient aussi très simple :

trait Foo {
    type Bar: Baz;
    // stuff...
}

struct Quux;

impl Foo for Quux {
    type Bar: Baz; // Oh look! Same as in the trait; I had to do nothing!
    // stuff...
}

La syntaxe impl Bar semble meilleure lorsque vous auriez autrement à introduire des temporaires, mais elle applique également la syntaxe de manière cohérente tout au long.

Pouvoir utiliser les deux syntaxes ne serait pas vraiment différent de pouvoir utiliser impl Trait en position d'argument ainsi que d'avoir un paramètre de type explicite T: Trait qui est ensuite utilisé par un argument.

EDIT1 : En fait, n'avoir qu'une seule syntaxe serait une casse spéciale, et non l'inverse.

@rpjohnst Je accord , même si j'aurais dû dire que cela n'a rien à voir explicitement avec les limites.

Quoi qu'il en soit, je ne suis pas contre la syntaxe type Foo: Bar; , mais pour l'amour de Dieu, débarrassons-nous du mot-clé abstract . type en soi est assez clair, en toutes circonstances.

Personnellement, je pense qu'utiliser = et impl est un bon indice visuel que l'inférence se produit. Cela permet également de repérer plus facilement ces endroits lorsque vous parcourez un fichier plus volumineux.

De plus, en supposant que je vois type Iter: Iterator<Item = Foo> je devrai trouver Foo et déterminer s'il s'agit d'un type ou d'un trait avant de savoir ce qui se passe.

Et enfin, je pense que l'indice visuel des points d'inférence aidera également à déboguer les erreurs d'inférence et à interpréter les messages d'erreur d'inférence.

Je pense donc que la variante = / impl résout un peu plus de problèmes de paperasserie.

@phaylon

De plus, en supposant que je vois le type Iter: Iterator<Item = Foo> je devrai trouver Foo et déterminer s'il s'agit d'un type ou d'un trait avant de savoir ce qui se passe.

Cela, je ne comprends pas ; Item = Foo devrait toujours être un type de nos jours étant donné que dyn Foo est stable (et que le trait nu est progressivement supprimé...) ?

@Centril

Cela, je ne comprends pas ; Item = Foo devrait toujours être un type de nos jours étant donné que dyn Foo est stable (et que le trait nu est en train de disparaître ...)?

Oui, mais dans la variante impl moins proposée, il pourrait s'agir d'un type inféré avec une limite ou d'un type concret. Par exemple, Iterator<Item = String> vs Iterator<Item = Display> . Je dois connaître les traits pour savoir si l'inférence se produit.

Edit : Ah, je n'ai pas remarqué que l'un d'entre eux utilisait : . Un peu ce que je veux dire par facile à manquer :) Mais vous avez raison, ils sont différents.

Edit 2: Je pense que ce problème se poserait en dehors des types associés. Compte tenu de type Foo: (Bar, Baz) vous auriez besoin de connaître Bar et Baz pour savoir où se produit l'inférence.

@Centril

EDIT1 : En fait, n'avoir qu'une seule syntaxe serait une casse spéciale, et non l'inverse.

Il n'y a actuellement qu'une seule façon de déclarer des types _existentiels_, -> impl Trait . Il existe deux manières de déclarer des types _universal_ ( T: Trait et : impl Trait dans une liste d'arguments).

Si nous avions des modules polymorphes qui acceptaient des types universels, je pourrais voir quelques arguments à ce sujet, mais je pense que l'utilisation actuelle de type Name = Type; dans les modules et les définitions de traits est un paramètre de type de sortie, qui devrait être un paramètre existentiel taper.


@phaylon

Oui, mais dans la variante moins impl proposée, il pourrait s'agir d'un type inféré avec une limite ou d'un type concret. Par exemple, Iterator<Item = String> vs Iterator<Item = Display> . Je dois connaître les traits pour savoir si l'inférence se produit.

Je crois que la variante impl moins utilise : Bound dans tous les cas pour les types existentiels, donc vous pourriez avoir Iterator<Item = String> ou Iterator<Item: Display> comme limites de trait, mais Iterator<Item = Display> serait une déclaration invalide.

@Nemo157
Vous avez raison en ce qui concerne le cas de type associé, ma mauvaise là. Mais (comme indiqué dans mon édition), je pense qu'il y a toujours un problème avec type Foo: (A, B) . Comme A ou B pourrait être un type ou un trait.

Je pense que c'est aussi une bonne raison d'opter pour = . Le : vous indique seulement que certaines choses sont déduites, mais ne vous dit pas lesquelles. type Foo = (A, impl B) me semble plus clair.

Je suppose également que la lecture et la fourniture d'extraits de code sont plus faciles avec impl , car un contexte supplémentaire sur ce qui est un trait et ce qui ne l'est pas n'a jamais besoin d'être fourni.

Edit: Quelques crédits: Mon argument est fondamentalement le même que celui de @alexreg ici , je voulais juste expliquer pourquoi je pense que impl est préférable.

Il n'y a actuellement qu'une seule façon de déclarer des types existentiels, -> impl Trait . Il existe deux façons de déclarer des types universels ( T: Trait et : impl Trait dans une liste d'arguments).

C'est ce que je dis :PI Pourquoi la quantification universelle aurait-elle deux voies mais existentielle une seule (ignorer dyn Trait ) dans d'autres endroits ?

Il me semble également probable qu'un utilisateur écrirait type Foo: Bound; et type Foo = impl Bound; ayant appris différentes parties du langage, et je ne peux pas dire qu'une syntaxe soit nettement meilleure dans tous les cas ; Il est clair pour moi qu'une syntaxe est meilleure pour certaines choses et une autre pour différentes choses.

@phaylon

Je crois que c'est aussi une bonne raison d'aller avec =. Le : vous indique seulement que certaines choses sont déduites, mais ne vous dit pas lesquelles. type Foo = (A, impl B) me semble plus clair.

Oui, c'est probablement une autre bonne raison. Il faut vraiment un peu de déballage pour comprendre ce qui est quantifié existentiellement – ​​sauter de définition en définition.

Une autre chose est : autoriserait-on même : dans une liaison de type associée, sous cette syntaxe ? Cela me semblerait être un cas spécial un peu étrange, étant donné que les types existentiels ne peuvent être composés/combinés d'aucune autre manière dans cette syntaxe proposée. J'imagine que ce qui suit serait l'approche la plus cohérente en utilisant cette syntaxe :

type A: Foo;
type B: Bar;
type C: Baz;
type D: Iterator<Item = C>; 
type E = (A, Vec<B>, D);

En utilisant la syntaxe que je (et quelques autres ici) préfère, nous pourrions écrire tout cela sur une seule ligne, et de plus, il est immédiatement clair où se passe la quantification !

type E = (impl Foo, Vec<impl Bar>, impl Iterator<Item = impl Baz>);

Sans rapport avec ce qui précède : quand jouons-nous pour implémenter let x: impl Trait tous les soirs ? Cela faisait un moment que je manquais cette fonctionnalité.

@alexreg

Une autre chose est : autoriserait-on même : dans une liaison de type associée, sous cette syntaxe ?

Oui pourquoi pas; Ce serait un effet naturel de rust-lang/rfcs#2289 + type Foo: Bound .

Tu pourrais aussi faire :

type E = (impl Foo, Vec<impl Bar>, impl Iterator<Item: Baz>);

@Centril Je pense que c'est une mauvaise idée d'autoriser deux syntaxes alternatives. Ça sent le syndrome du « nous ne pouvions pas décider, alors nous allons simplement soutenir les deux ». Voir du code qui les mélange et les associe sera une véritable horreur !

@Centril, je suis un peu avec @nikomatsakis sur votre RFC BTW, désolé. J'écrirais plutôt impl Iterator<Item = impl Baz> . Agréable et explicite.

@alexreg C'est juste ;

Mais (mal)heureusement (selon votre PDV), nous avons déjà commencé le "autoriser deux syntaxes alternatives" avec impl Trait en position d'argument, de sorte que nous avons à la fois Foo: Bar et impl Bar travaillant pour signifier la même chose ;

C'est pour la quantification universelle, mais la notation impl Trait ne se soucie pas vraiment de quel côté de la dualité elle se trouve ; après tout, nous n'avons pas choisi any Trait et some Trait .

Etant donné qu'on a déjà fait le choix "on ne pouvait pas décider" et "le côté de la dualité n'a pas d'importance syntaxique" , il me semble cohérent d'appliquer "on ne peut pas décider" partout pour que l'utilisateur n'ait pas en "mais je pourrais l'écrire comme ça là-bas, pourquoi pas ici ?" ;)


PS :

Ré. impl Iterator<Item = impl Baz> cela ne fonctionne pas comme une limite dans une clause where ; donc vous devriez le mélanger comme Iter: Iterator<Item = impl Baz> . Vous devez autoriser : Iter = impl Iterator<Item = impl Baz> pour que cela fonctionne uniformément (peut-être devrions-nous ?).

Utiliser : Bound est également au lieu de = impl Bound est également explicite, juste plus court ^,-
Je pense que la différence d'espacement entre X = Ty et X: Ty rend la syntaxe lisible.

Ayant ignoré mes propres conseils, continuons cette conversation au RFC ;)

Mais (mal)heureusement (en fonction de votre PDV), nous avons déjà commencé à "autoriser deux syntaxes alternatives" avec impl Trait en position d'argument, de sorte que nous avons à la fois Foo: Bar et impl Bar travaillant pour signifier la même chose;

Nous l'avons fait, mais je crois que le choix a été fait davantage d'un point de vue de symétrie/cohérence. Les arguments de type générique sont strictement plus puissants que ceux de type universel ( impl Trait ). Mais nous introduisions impl Trait dans la position de retour, il était logique de lui introduire la position d'argument.

Etant donné qu'on a déjà fait le choix "on ne peut pas décider" et "le côté de la dualité n'a pas d'importance syntaxiquement", il me semble cohérent d'appliquer "on ne peut pas décider" partout pour que l'utilisateur n'ait pas en "mais je pourrais l'écrire comme ça là-bas, pourquoi pas ici ?" ;)

Je ne suis pas sûr que ce soit au point où nous devrions lever les bras et dire "mettons tout en œuvre". Il n'y a pas d'argument aussi clair ici quant au gain.

PS :

Ré. impl Iterator<Item = impl Baz> cela ne fonctionne pas comme une limite dans une clause where ; donc vous devriez le mélanger comme Iter: Iterator<Item = impl Baz> . Vous devez permettre : Iter = impl Iterator<Item = impl Baz> pour que cela fonctionne uniformément (peut-être devrions-nous ?).

Je dirais que nous soutenons simplement where Iter: Iterator<Item = T>, T: Baz (comme nous l'avons fait maintenant) ou allons jusqu'au bout avec Iter = impl Iterator<Item = impl Baz> (comme vous l'avez suggéré). Autoriser uniquement la maison de transition semble un peu une échappatoire.

Utiliser : Bound est également au lieu de = impl Bound est également explicite, juste plus court ^,-
Je pense que la différence d'espacement entre X = Ty et X: Ty rend la syntaxe lisible.

C'est lisible, mais je ne pense pas qu'il soit aussi clair/explicite qu'un type existentiel soit utilisé. Ceci est exacerbé lorsque la définition doit être divisée sur plusieurs lignes en raison de la limitation de cette syntaxe.

Ayant ignoré mes propres conseils, continuons cette conversation au RFC ;)

Attendez, vous voulez dire votre RFC ? Je pense que c'est pertinent à la fois pour ça et pour celui-ci, d'après ce que je peux dire. :-)

Attendez, vous voulez dire votre RFC ? Je pense que c'est pertinent à la fois pour ça et pour celui-ci, d'après ce que je peux dire. :-)

D'ACCORD; Continuons ici alors;

Nous l'avons fait, mais je crois que le choix a été fait davantage d'un point de vue de symétrie/cohérence. Les arguments de type générique sont strictement plus puissants que ceux de type universel ( impl Trait ). Mais nous introduisions impl Trait dans la position de retour, il était logique de lui introduire la position d'argument.

Tout mon propos concerne la cohérence et la symétrie. =P
Si vous êtes autorisé à écrire impl Trait fois pour la quantification existentielle et universelle, il me semble logique que vous soyez également autorisé à utiliser Type: Trait pour la quantification universelle et existentielle.

Concernant le pouvoir expressif, le premier est plus puissant que le second comme vous le dites, mais cela ne doit pas nécessairement être le cas ; Ils pourraient être tout aussi puissants si nous voulions qu'ils soient AFAIK (mais je ne dis absolument pas que nous devrions le faire..).

fn foo(bar: impl Trait, baz: typeof bar) { // eww... but possible!
    ...
}

Je ne suis pas sûr que ce soit au point où nous devrions lever les bras et dire "mettons tout en œuvre". Il n'y a pas d'argument aussi clair ici quant au gain.

Mon argument est que surprendre les utilisateurs avec "Cette syntaxe est utilisable ailleurs et sa signification est claire ici, mais vous ne pouvez pas l'écrire à cet endroit" coûte plus cher que d'avoir deux façons de le faire (avec lesquelles vous devez tous les deux vous familiariser de toute façon ). Nous avons fait des choses similaires avec https://github.com/rust-lang/rfcs/pull/2300 (fusionné), https://github.com/rust-lang/rfcs/pull/2302 (PFCP), https ://github.com/rust-lang/rfcs/pull/2175 (fusionné) où nous remplissons les trous de cohérence même s'il était possible d'écrire d'une autre manière auparavant.

C'est lisible, mais je ne pense pas qu'il soit aussi clair/explicite qu'un type existentiel soit utilisé.

Lisible est suffisant à mon avis; Je ne pense pas que Rust attribue à « explicite avant tout » et être trop verbeux (ce que je trouve que la syntaxe est trop utilisée) coûte également en décourageant l'utilisation.
(Si vous voulez que quelque chose soit utilisé souvent, donnez-lui une syntaxe plus rapide... cf ? comme pot-de-vin contre .unwrap() ).

Ceci est exacerbé lorsque la définition doit être divisée sur plusieurs lignes en raison de la limitation de cette syntaxe.

Cela, je ne comprends pas ; Il me semble que Assoc = impl Trait devrait provoquer des séparations de ligne encore plus que Assoc: Trait simplement parce que le premier est plus long.

Je dirais que nous soutenons simplement where Iter: Iterator<Item = T>, T: Baz (comme nous l'avons fait maintenant) ou allons jusqu'au bout avec Iter = impl Iterator<Item = impl Baz> (comme vous l'avez suggéré).
Autoriser uniquement la maison de transition semble un peu une échappatoire.

Exactement !, n'allons pas à mi-chemin / échappatoire et implémentons where Iter: Iterator<Item: Baz> ;)

@Centril D'accord, vous m'avez

J'éditerai ceci avec ma réponse complète demain.

Éditer

Comme le souligne @Centril , nous prenons déjà en charge les types universels en utilisant la syntaxe : Trait (liée). par exemple

fn foo<T: Trait>(x: T) { ... }

à côté des types universels « propres » ou « réifiés », par exemple

fn foo(x: impl Trait) { ... }

Bien sûr, le premier est plus puissant que le second, mais le second est plus explicite (et sans doute plus lisible) quand c'est tout ce qui est requis. En fait, je crois fermement que nous devrions avoir un compilateur en faveur de cette dernière forme lorsque cela est possible.

Maintenant, nous avons déjà impl Trait dans la position de retour de la fonction, ce qui représente un type existentiel. Les types de traits associés sont de forme existentielle et utilisent déjà la syntaxe : Trait .

Ceci, étant donné l'existence de ce que je vais à la fois les formes propres et liées des types universels dans Rust à l'heure actuelle, et de même l'existence de formes propres et liées pour les types existentiels (ces derniers uniquement dans les traits à l'heure actuelle), je crois fermement que nous devrions étendre la prise en charge à la fois des formes propres et liées des types existentiels à l'extérieur des traits. C'est-à-dire que nous devons prendre en charge les éléments suivants à la fois de manière générale et pour les types associés .

type A: Iterator<Item: Foo + Bar>;
type B = (impl Baz, impl Debug, String);

J'appuie également le comportement de linting du compilateur suggéré dans ce commentaire , qui devrait fortement réduire la variation d'expression des types existentiels courants dans la nature.

Je pense toujours que confondre la quantification universelle et exxisistentielle sous un seul mot-clé était une erreur, et donc cet argument de cohérence ne fonctionne pas pour moi. La seule raison pour laquelle un seul mot-clé fonctionne dans les signatures de fonction est que le contexte vous contraint nécessairement à n'utiliser qu'une seule forme de quantification dans chaque position. Il y a des sucres potentiels que je pourrais voir comme un truc où tu n'as pas les mêmes contraintes

struct Foo {
    pub foo: impl Display,
}

Est-ce un raccourci pour la quantification existentielle ou universelle ? D'après l'intuition dérivée de l'utilisation de impl Trait dans les signatures de fonctions, je ne vois pas comment vous pourriez décider. Si vous essayez réellement de l'utiliser comme les deux, vous vous rendrez rapidement compte que la quantification universelle anonyme dans cette position est inutile, il doit donc s'agir d'une quantification existentielle, mais cela semble incompatible avec impl Trait dans les arguments de fonction.

Ce sont deux opérations fondamentalement différentes, oui, elles utilisent toutes les deux des limites de traits, mais je ne vois aucune raison pour laquelle le fait d'avoir deux façons de déclarer un type existentiel réduirait la confusion pour les nouveaux arrivants. Si essayer d'utiliser type Name: Trait est en fait une chose probable pour les nouveaux arrivants, cela pourrait être résolu via un lint :

    type Foo: Display;
    ^^^^^^^^^^^^^^^^^^
note: were you attempting to create an existential type?
note: suggested replacement `type Foo = impl Display`

Et je viens de proposer une formulation alternative de votre argument à laquelle je serais beaucoup plus disposé, il faudra attendre que je sois devant un vrai ordinateur pour relire la RFC et publier à ce sujet.

J'ai l'impression que je n'ai pas encore assez d'expérience avec Rust pour commenter les RFC. Cependant, je suis intéressé à voir cette fonctionnalité fusionnée dans Rust nocturne et stable, afin de l'utiliser avec Rust libp2p pour créer un protocole de partitionnement pour Ethereum dans le cadre de l' implémentation de partitionnement Drops of Diamond . Je me suis abonné au numéro, mais je n'ai pas le temps de suivre tous les commentaires ! Comment puis-je rester à jour à un niveau élevé sur cette question, sans avoir à parcourir les commentaires ?

J'ai l'impression que je n'ai pas encore assez d'expérience avec Rust pour commenter les RFC. Cependant, je suis intéressé à voir cette fonctionnalité fusionnée dans Rust nocturne et stable, afin de l'utiliser avec Rust libp2p pour créer un protocole de partitionnement pour Ethereum dans le cadre de l' implémentation de partitionnement Drops of Diamond . Je me suis abonné au numéro, mais je n'ai pas le temps de suivre tous les commentaires ! Comment puis-je rester à jour à un niveau élevé sur cette question, sans avoir à parcourir les commentaires ? Pour le moment, il semble que je doive simplement le faire en me connectant de temps en temps et en ne m'abonnant pas au problème. Ce serait bien si je pouvais m'abonner par e-mail pour avoir des nouvelles de haut niveau à ce sujet.

Je crois toujours que confondre la quantification universelle et existentielle sous un seul mot-clé était une erreur, et donc cet argument de cohérence ne fonctionne pas pour moi.

En tant que principe général, indépendant de cette caractéristique, je trouve ce raisonnement problématique.

Je crois que nous devrions aborder la conception linguistique à partir de la façon dont une langue est plutôt que de la façon dont nous aurions souhaité qu'elle soit dans le cadre d'un déroulement alternatif de l'histoire. La syntaxe impl Trait tant que quantification universelle en position d'argument est stabilisée, vous ne pouvez donc pas la souhaiter. Même si vous pensez que X, Y et Z étaient des erreurs (et je pourrais trouver beaucoup de choses que je pense personnellement être des erreurs dans la conception de Rust, mais je les accepte et les assume...), nous devons vivre avec elles maintenant, et je pense sur la façon dont nous pouvons tout faire s'emboîter compte tenu de la nouvelle fonctionnalité (rendre les choses cohérentes).

Dans la discussion, je pense que l'ensemble du corpus des RFC et le langage en l'état devraient être pris comme si ce n'était pas des axiomes, alors des arguments forts.


Vous pourriez faire valoir (mais je ne le ferais pas) que :

struct Foo {
    pub foo: impl Display,
}

est sémantiquement équivalent à :

struct Foo<T: Display> {
    pub foo: T,
}

sous le raisonnement fonction-argument.

Fondamentalement, étant donné impl Trait , vous devez penser "est-ce que ce type de retour est similaire ou que cet argument est similaire?" , ce qui peut être difficile.


Si essayer d'utiliser type Name: Trait est en fait une chose probable pour les nouveaux arrivants, cela pourrait être résolu via un lint :

Je voudrais aussi pelucher, mais dans l'autre sens ; Je pense que les manières suivantes devraient être idiomatiques :

// GOOD:
type Foo: Iterator<Item: Display>;

type Bar = (impl Display, impl Debug);

// BAD
type Foo = impl Iterator<Item = impl Display>;

type Bar0: Display;
type Bar1: Debug;
type Bar = (Bar0, Bar1);

D'accord, une formulation alternative à laquelle je pense que la RFC 2071 fait allusion et qui a peut-être été discutée dans le problème, mais n'a jamais été explicitement déclarée :

Il n'y a qu'une seule façon de déclarer des types quantifiés existentiellement : existential type Name: Bound; (en utilisant existential car cela est spécifié dans la RFC, je ne suis pas entièrement contre la suppression du mot-clé sous cette formulation).

Il existe en outre du sucre pour déclarer implicitement un type quantifié existentiellement sans nom dans la portée actuelle : impl Bound (en ignorant le sucre de quantification universel dans les arguments de fonction pour le moment).

Ainsi, l'utilisation actuelle du type de retour est un simple désucrage :

fn foo() -> impl Iterator<Item = impl Display> { ... }
existential type _0: Display;
existential type _1: Iterator<Item = _0>;
fn foo() -> _1 { ... }

étendre à const , static et let est tout aussi trivial.

La seule extension non mentionnée dans le RFC est : prendre en charge ce sucre dans la syntaxe type Alias = Concrete; , donc lorsque vous écrivez

type Foo = impl Iterator<Item = impl Display>;

c'est en fait du sucre pour

existential type _0: Display;
existential type _1: Iterator<Item = _0>;
type Foo = _1;

qui s'appuie ensuite sur la nature transparente des alias de type pour permettre au module actuel de parcourir Foo et de voir qu'il fait référence à un type existentiel.

En fait, je crois fermement que nous devrions avoir un compilateur en faveur de cette dernière forme lorsque cela est possible.

Je suis principalement d'accord avec le commentaire de @alexreg , mais j'ai quelques inquiétudes concernant le linting vers arg: impl Trait , principalement en raison du risque d'encourager des changements de rupture dans les bibliothèques puisque impl Trait ne fonctionne pas avec turbofish (en ce moment, et vous auriez besoin d'un turbofish partiel pour que cela fonctionne bien). Par conséquent, le linting dans clippy semble moins simple que dans le cas des alias de type (où il n'y a pas de turbofish pour causer des problèmes).

Je suis principalement d'accord avec le commentaire de @alexreg , mais j'ai quelques inquiétudes concernant le linting vers arg: impl Trait, principalement en raison du risque d'encourager des changements de rupture dans les bibliothèques puisque impl Trait ne fonctionne pas avec les turbofish (pour le moment, et vous auriez besoin d'un turbofish partiel pour que cela fonctionne bien). Par conséquent, le linting dans clippy semble moins simple que dans le cas des alias de type (où il n'y a pas de turbofish pour causer des problèmes).

@Centril vient de

Donc... nous avons eu pas mal de discussions sur la syntaxe des types existentiels nommés maintenant. Devrions-nous essayer de parvenir à une conclusion et de l'écrire dans le message RFC / PR, afin que quelqu'un puisse commencer à travailler sur la mise en œuvre réelle ? :-)

Personnellement, une fois que nous avons nommé les existentiels, je préférerais une peluche (le cas échéant) loin de toute utilisation de impl Trait n'importe où .

@rpjohnst Eh bien, vous êtes certainement d'accord avec moi et @Centril en ce qui concerne les existentiels nommés... Tout dépend si l'on souhaite privilégier la simplicité ou la généralité dans ce contexte.

Le RFC sur impl Trait en position d'argument est-il à jour ? Si c'est le cas, est-il prudent d'affirmer que sa sémantique est _universelle_ ? Si oui : j'ai envie de pleurer. Profondément.

@phaazon : Notes de version de Rust 1.26 pour impl Trait :

Note latérale pour vous, théoriciens du type : ce n'est pas un existentiel, c'est toujours un universel. En d'autres termes, impl Trait est universel en position d'entrée, mais existentiel en position de sortie.

Juste pour exprimer mes pensées à ce sujet:

  • Nous avions déjà une syntaxe pour les variables de type et vraiment, il y a en fait quelques utilisations pour les variables de type anynomous (c'est-à-dire que c'est très souvent que vous voulez utiliser la variable de type à plusieurs endroits au lieu de simplement la déposer à un seul endroit).
  • Les existentiels covariants nous ouvriraient les portes des fonctions de rang n, quelque chose qui est difficile à faire en ce moment sans trait (voir ceci ) et c'est une fonctionnalité qui manque vraiment à Rust.
  • impl Trait est facile à désigner comme « type choisi par l'appelé », parce que… parce que c'est la seule construction de langage pour l'instant qui nous permet de le faire ! Le choix du type par l'appelant est déjà disponible via plusieurs constructions.

Je pense vraiment que la décision actuelle de impl Trait en position d'argument est dommage. :cri:

Je pense vraiment que l'impl Trait dans la position argumentative de la décision actuelle est dommage. ??

Bien que je sois un peu déchiré à ce sujet, je pense certainement qu'il serait préférable de consacrer du temps à la mise en œuvre de let x: impl Trait dès maintenant !

Les existentielles covariantes nous ouvriraient les portes des fonctions de rang n

Nous avons déjà une syntaxe pour cela ( fn foo(f: impl for<T: Trait> Fn(T)) ), (alias "type HRTB"), mais elle n'est pas encore implémentée. fn foo(f: impl Fn(impl Trait)) produit une erreur indiquant que "les impl Trait imbriqués ne sont pas autorisés", et je pense que nous voudrons que cela signifie la version de rang supérieur, lorsque nous obtenons le type HRTB.

C'est similaire à la façon dont Fn(&'_ T) signifie for<'a> Fn(&'a T) , donc je ne m'attends pas à ce que ce soit controversé.

En regardant le brouillon actuel, impl Trait en position d'argument est un _universel_, mais vous dites que impl for<_> Trait transforme en un _existentiel_ ?! C'est fou ?

Pourquoi avons-nous pensé que nous avions besoin d'introduire _encore une autre manière_ de construire un _universel_ ? Je veux dire:

fn foo(x: impl MyTrait)

N'est intéressant que parce que la variable de type anonyme n'apparaît qu'une seule fois dans le type . Si vous devez retourner le même type :

fn foo(x: impl Trait) -> impl Trait

Ne fonctionnera évidemment pas. Nous disons aux gens de passer d'un idiome plus général à un aucune valeur ajoutée - l'hypothèse d'apprentissage que j'ai lue dans la RFC est un argument étrange où nous pensons à la place des nouveaux arrivants - ils auront toujours besoin d'apprendre des choses, alors pourquoi rendons-nous la syntaxe plus ambiguë à peu près tout le monde afin de réduire la courbe d'apprentissage ici ? Lorsque les gens s'habitueront à l'universel par rapport à l'existentiel (et en arriveront un, le principe est très simple avec les bons mots), les gens commenceront à se demander pourquoi nous avons le même mot-clé / les mêmes modèles pour exprimer les deux et aussi where et les paramètres de modèle en place.

Argh, je suppose que tout cela a déjà été accepté et je fulmine pour rien. Je pense juste que c'est vraiment dommage. Je suis sûr que je ne suis pas le seul déçu par la décision du RFC.

(Il ne sert probablement pas à grand-chose de continuer à débattre de cela une fois que la fonctionnalité a été stabilisée, mais voyez ici un argument convaincant (avec lequel je suis d'accord) pour expliquer pourquoi impl Trait en position d'argument ayant la sémantique qu'il fait est sensé et cohérent. Tl;dr c'est pour la même raison pour laquelle fn foo(arg: Box<Trait>) fonctionne à peu près de la même manière que fn foo<T: Trait>(arg: Box<T>) , même si dyn Trait est un existentiel ; remplacez maintenant dyn avec impl .)

En regardant le brouillon actuel, impl Trait en position d'argument est un universel, mais vous dites que impl for<_> Trait transforme en un existentiel ?!

Non, ils sont tous les deux universels. Je dis que les utilisations les mieux classées ressembleraient à ceci :

fn foo<F: for<G: Fn(X) -> Y> Fn(G) -> Z>(f: F) {...}

qui pourrait, en même temps qu'il est ajouté (c'est-à-dire sans modification de impl Trait ) s'écrire comme :

fn foo(f: impl for<G: Fn(X) -> Y> Fn(G) -> Z) {...}

C'est universel impl Trait , juste que le Trait est un HRTB (similaire à impl for<'a> Fn(&'a T) ).
Si nous décidons (ce qui est probable) que impl Trait intérieur des arguments Fn(...) est également universel, vous pouvez écrire ceci pour obtenir le même effet :

fn foo(f: impl Fn(impl Fn(X) -> Y) -> Z) {...}

C'est ce que je pensais que vous vouliez dire par "plus haut placé", si ce n'est pas le cas, faites-le moi savoir.

Une décision encore plus intéressante pourrait être d'appliquer le même traitement en position existentielle, c'est-à-dire d'autoriser cela (ce qui signifierait "retourner une fermeture qui prend n'importe quelle autre fermeture") :

fn foo() -> impl for<G: Fn(X) -> Y> Fn(G) -> Z {...}

à écrire comme ceci :

fn foo() -> impl Fn(impl Fn(X) -> Y) -> Z {...}

Ce serait un impl Trait existentiel contenant un impl Trait universel (lié à l'existentiel, au lieu de la fonction englobante).

@eddyb Ne
Le mot-clé de la quantification existentielle ne serait-il pas aussi réutilisable pour les types existentiels ?
Pourquoi utilisons-nous impl pour la quantification existentielle ( et universelle) mais existential pour les types existentiels ?

Je voudrais faire trois points :

  • Il n'y a pas grand intérêt à discuter si impl Trait est existentiel ou universel. La plupart des programmeurs n'ont probablement pas lu assez de manuels de théorie des types. La question devrait être de savoir si les gens l'aiment ou s'ils trouvent cela déroutant. Pour répondre à cette question, une forme de rétroaction peut être consultée à la fois ici dans ce fil, sur reddit ou sur le forum . Si quelque chose doit être expliqué plus en détail, il échoue à un test décisif pour une fonctionnalité intuitive ou non surprenante. Nous devrions donc regarder combien de personnes et à quel point elles sont confuses et s'il y a plus de questions qu'avec d'autres fonctionnalités. C'est en effet triste que ce retour arrive après stabilisation et quelque chose devrait être fait à propos de ce phénomène, mais c'est pour une discussion séparée.
  • Techniquement, même après stabilisation, il y aurait moyen de se débarrasser de la fonctionnalité dans ce cas (en laissant de côté la décision s'il le fallait). Il serait possible de lésiner contre les fonctions d'écriture qui l'utilisent et de supprimer la capacité dans la prochaine édition (tout en préservant la possibilité de les appeler si elles proviennent de caisses d'éditions différentes). Cela satisferait aux garanties de stabilité à la rouille.
  • Non, ajouter deux mots-clés supplémentaires pour spécifier les types existentiels et universels n'améliorerait pas la confusion, cela ne ferait qu'empirer les choses.

C'est effectivement dommage que ce retour arrive après stabilisation et il faudrait faire quelque chose contre ce phénomène

Il y a eu des objections à impl Trait dans la position d'argument tant que c'était une idée. Des commentaires comme celui-ci _ne sont pas nouveaux_, ils ont été très débattus même dans le fil RFC correspondant. Il y a eu beaucoup de discussions non seulement sur les types universels/existentiels du point de vue de la théorie des types, mais aussi sur la façon dont cela serait déroutant pour les nouveaux utilisateurs.

Certes, nous n'avons pas eu le point de vue des nouveaux utilisateurs, mais cela ne vient pas de nulle part.

@Boscop any et some ont été proposés comme une paire de mots-clés pour faire ce travail mais ont été rejetés (bien que je ne sache pas si la justification a déjà été écrite quelque part).

Certes, nous n'avons pas pu obtenir de retours de personnes novices en matière de rouille et qui n'étaient pas des théoriciens des types

Et l'argument en faveur de l' inclusion a toujours été que cela faciliterait la tâche des nouveaux arrivants. Donc, si nous avons maintenant des retours réels de nouveaux arrivants, ne devrait-il pas s'agir d'un retour d'information très pertinent au lieu de discuter de la façon dont les nouveaux arrivants devraient le comprendre ?

Je suppose que si quelqu'un avait le temps, une sorte de recherche sur les forums et d'autres endroits pourrait être faite à quel point les gens étaient confus avant et après l'inclusion (je n'étais pas très bon en statistiques, mais je suis presque sûr que quelqu'un qui l'était pourrait proposer quelque chose de mieux que des prédictions aveugles).

Et l'argument en faveur de l'inclusion a toujours été que cela faciliterait la tâche des nouveaux arrivants. Donc, si nous avons maintenant des retours réels de nouveaux arrivants, ne devrait-il pas s'agir d'un retour d'information très pertinent au lieu de discuter de la façon dont les nouveaux arrivants devraient le comprendre ?

Oui? Je veux dire que je ne discute pas si ce qui s'est passé était une bonne ou une mauvaise idée. Je veux juste souligner que le fil RFC a reçu des commentaires à ce sujet, et cela a été décidé de toute façon.

Comme vous l'avez dit, il est probablement préférable d'avoir la méta-discussion sur les commentaires ailleurs, bien que je ne sache pas où ce serait.

Non, ajouter deux mots-clés supplémentaires pour spécifier les types existentiels et universels n'améliorerait pas la confusion, cela ne ferait qu'empirer les choses.

Pire? Comment est-ce ainsi ? Je préfère avoir plus à retenir que l'ambiguïté/la confusion.

Oui? Je veux dire que je ne discute pas si ce qui s'est passé était une bonne ou une mauvaise idée. Je veux juste souligner que le fil RFC a reçu des commentaires à ce sujet, et cela a été décidé de toute façon.

Sûr. Mais les deux parties étaient des programmeurs âgés, marqués et expérimentés avec une compréhension profonde de ce qui se passe sous le capot, devinant un groupe dont ils ne font pas partie (nouveaux arrivants) et devinant l'avenir. D'un point de vue factuel, ce n'est pas beaucoup mieux que de lancer des dés en ce qui concerne ce qui se passe réellement dans la réalité. Il ne s'agit pas d'une expérience insuffisante des experts, mais de l'absence de données adéquates sur lesquelles fonder les décisions.

Maintenant, il a été introduit et nous avons un moyen d'obtenir les données concrètes réelles, ou des données aussi précises que possible sur le nombre de personnes confuses sur une échelle de 0 à 10.

Comme vous l'avez dit, il est probablement préférable d'avoir la méta-discussion sur les commentaires ailleurs

Par exemple ici, j'ai déjà commencé une telle discussion et il y a quelques mesures réelles qui peuvent être prises, même si petites : https://internals.rust-lang.org/t/idea-mandate-n-independent-uses -avant-de-stabiliser-une-fonction/7522/14. Je n'ai pas eu le temps d'écrire le RFC, donc si quelqu'un me bat ou veut m'aider, ça ne me dérangera pas.

Pire? Comment est-ce ainsi ?

Parce que, à moins que impl Trait soit obsolète, vous avez les 3, donc vous avez plus à retenir en plus de la confusion. Si impl Trait devait disparaître, la situation serait différente et ce serait une pondération des avantages et des inconvénients des deux approches.

impl Trait comme dans le callee-picking serait suffisant. Si vous essayez de l'utiliser en position d'argument, alors vous introduisez la confusion. Les HRTB élimineraient cette confusion.

@vorner Auparavant, j'ai soutenu que nous devrions effectuer des tests A/B réels avec les débutants de Rust pour voir ce qu'ils trouvent réellement plus facile et plus difficile à apprendre, car il est difficile de deviner en tant que personne qui maîtrise Rust.
FWIW, je me souviens, quand j'apprenais Rust (venant de C++, D, Java, etc.), les génériques de type quantification universelle (y compris leur syntaxe) étaient faciles à comprendre (les durées de vie dans les génériques étaient un peu plus difficiles).
Je pense que impl Trait pour les types d'arguments entraînera beaucoup de confusion de la part des débutants et de nombreuses questions comme celle-ci .
En l'absence de toute preuve indiquant que les changements rendraient Rust plus facile à apprendre, nous devrions nous abstenir d'apporter de tels changements, et à la place faire des changements qui rendent/maintiennent Rust plus cohérent car la cohérence le rend au moins facile à retenir. Les débutants de Rust devront lire le livre plusieurs fois de toute façon, donc introduire impl Trait pour args pour permettre de reporter les génériques dans le livre à plus tard n'enlève pas vraiment de complexité.

@eddyb Btw, pourquoi avons-nous besoin d'un autre mot-clé existential pour les types en plus de impl ? (Je souhaite que nous utilisions some pour les deux..)

FWIW, je me souviens, quand j'apprenais Rust (venant de C++, D, Java, etc.), les génériques de type quantification universelle (y compris leur syntaxe) étaient faciles à comprendre (les durées de vie dans les génériques étaient un peu plus difficiles).

Moi non plus, je ne pense pas que ce soit un problème. Dans mon entreprise actuelle, j'anime des cours Rust ‒ pour l'instant nous nous rencontrons une fois par semaine et j'essaie d'enseigner en mise en pratique. Les gens sont des programmeurs chevronnés, venant principalement de Java et Scala. Bien qu'il y ait eu quelques barrages routiers, les génériques (au moins les lire – ils sont un peu prudents avant de les écrire) dans la position d'argument n'était pas un problème. Il y a eu une petite surprise à propos des génériques en position de retour (par exemple, l'appelant choisit ce que la fonction renvoie), surtout qu'il peut souvent être élidé, mais l'explication a pris environ 2 minutes avant de cliquer. Mais j'ai même peur de mentionner l'existence d'impl Trait en position d'argument, car maintenant je devrais répondre à la question pourquoi il existe ‒ et je n'ai pas vraiment de réponse à cela. Ce n'est pas bon pour la motivation et la motivation est cruciale pour le processus d'apprentissage.

La question est donc de savoir si la communauté a suffisamment de voix pour rouvrir le débat avec des données pour étayer les arguments ?

@eddyb Btw, pourquoi avons-nous besoin d'un autre mot-clé existentiel pour les types en plus de impl ? (Je souhaite que nous en utilisions pour les deux..)

Pourquoi pas forall … /me s'éclipse lentement

@phaazon Nous avons forall (c'est-à-dire "universel") et c'est for , par exemple dans HRTB ( for<'a> Trait<'a> ).

@eddyb Oui, puis utilisez-le aussi pour l' existentiel , comme Haskell le fait avec forall , par exemple.

Toute la discussion est très opiniâtre, je suis un peu surpris que l'idée d'argument ait été stabilisée. J'espère qu'il y a un moyen de pousser un autre RFC plus tard pour annuler cela (je suis tout à fait prêt à l'écrire parce que je n'aime vraiment vraiment pas toute la confusion que cela va apporter).

Je ne comprends pas vraiment. Quel est l'intérêt de les avoir en position d'argumentation ? Je n'écris pas beaucoup de Rust, mais j'ai vraiment aimé pouvoir faire -> impl Trait . Quand l'utiliserais-je dans la position d'argument ?

J'avais cru comprendre que c'était surtout pour la cohérence. Si je peux écrire le type impl Trait dans une position d'argument dans une signature fn, pourquoi ne puis-je pas l'écrire ailleurs ?

Cela dit, j'aurais personnellement préféré dire aux gens "juste utiliser un paramètre de type"...

Oui, c'est pour la cohérence. Mais je ne suis pas sûr que ce soit un argument suffisant, lorsque les paramètres de type sont si faciles à utiliser. De plus, se pose alors le problème de savoir pour/contre lequel pelucher !

De plus, se pose alors le problème de savoir pour/contre lequel pelucher !

Étant donné que vous ne pouvez pas du tout exprimer plusieurs choses avec impl Trait , fonction avec impl Trait car l'un des arguments ne peut pas faire de turbofish et donc vous ne pouvez pas prendre son adresse (ai-je oublié quelques autre inconvénient ?), Je pense que cela n'a pas de sens de comparer les paramètres de type, car vous devez de toute façon les utiliser.

donc tu ne peux pas prendre son adresse

Vous pouvez, en le faisant déduire de la signature.

Quel est l'intérêt de les avoir en position d'argumentation ?

Il n'y en a pas car c'est exactement la même chose que d'utiliser un trait lié.

fn foo(x: impl Debug)

Est-ce exactement la même chose que

fn foo<A>(x: A) where A: Debug
fn foo<A: Debug>(x: A)

Considérez également ceci :

fn foo<A>(x: A) -> A where A: Debug

impl Trait en position d'argument ne vous permet pas de le faire car il est anonymisé . C'est alors une fonctionnalité assez inutile car nous avons déjà tout ce qu'il faut pour faire face à de telles situations. Les gens n'apprendront pas facilement cette nouvelle fonctionnalité car à peu près tout le monde connaît les variables de type / paramètres de modèle et Rust est le seul langage utilisant cette syntaxe impl Trait . C'est pourquoi beaucoup de gens disent qu'il aurait dû rester aux liaisons valeur de retour / let, car cela a introduit une nouvelle sémantique nécessaire (c'est-à-dire le type choisi par l'appelé).

Pour faire court, @iopq : vous n'en aurez pas besoin, et il n'y a rien d'autre que "Ajoutons une autre construction syntaxique de sucre dont personne n'aura réellement besoin car elle fait face à une utilisation très spécifique - c'est-à-dire des variables de type anonymisées" .

Aussi, quelque chose que j'ai oublié de dire : il est beaucoup plus difficile de voir comment votre fonction est paramétrée/monomorphisée.

@Verner Avec un turbofish partiel, il est tout à fait logique de l'effleurer par souci de simplicité, de lisibilité, d'explicitation. Je ne suis pas vraiment pour la fonctionnalité en position arg pour commencer cependant.

Comment est-ce cohérent lorsque -> impl Trait x: impl Trait l'appelé choisit le type, alors que dans

Je comprends qu'il n'y a pas d'autre moyen pour que cela fonctionne, mais cela ne semble pas "cohérent", cela semble le contraire de cohérent

Je suis vraiment d'accord que c'est tout sauf cohérent et que les gens seront confus, les nouveaux arrivants ainsi que les rustacés avancés et compétents.

Nous avons eu deux RFC, qui ont reçu un total de près de 600 commentaires entre eux depuis plus de 2 ans, pour résoudre les questions soulevées sur ce fil :

  • rust-lang/rfcs#1522 ("Minimal impl Trait ")
  • rust-lang/rfcs#1951 ("Finaliser la syntaxe et la portée des paramètres pour impl Trait , tout en l'étendant aux arguments")

(Si vous lisez ces discussions, vous verrez que j'étais au départ un fervent partisan de l'approche à deux mots-clés. Je pense maintenant qu'utiliser un seul mot-clé est la bonne approche.)

Après 2 ans et des centaines de commentaires, une décision a été prise et la fonctionnalité est maintenant stabilisée. C'est le problème de suivi de la fonctionnalité, qui est ouverte pour suivre les cas d'utilisation encore instables pour impl Trait . Religier les aspects réglés de impl Trait est hors sujet pour ce problème de suivi. Vous êtes invités à continuer à en parler, mais s'il vous plaît ne pas sur le suivi des problèmes.

Comment a-t-il été stabilisé lorsque impl Trait n'a même pas obtenu de support en position d'argument pour fns dans les traits ??

@daboross Ensuite, la case à cocher dans le message d'origine doit être cochée !

(Je viens de découvrir que https://play.rust-lang.org/?gist=47b1c3a3bf61f33d4acb3634e5a68388&version=stable fonctionne actuellement)

Je pense que c'est bizarre que https://play.rust-lang.org/?gist=c29e80715ac161c6dc95f96a7f91aa8c&version=stable&mode=debug ne fonctionne pas (encore), de plus avec ce message d'erreur. Suis-je le seul à penser ainsi ? Peut-être faudrait-il ajouter une case à cocher pour impl Trait en position de retour dans les traits, ou était-ce une décision consciente de n'autoriser que impl Trait en position d'argument pour les fonctions de trait, forçant l'utilisation de existential type pour les types de retour ? (ce qui… me semblerait incohérent, mais peut-être que je manque un point ?)

@Ekleog

Était-ce une décision consciente de n'autoriser impl Trait qu'en position d'argument pour les fonctions de trait, forçant l'utilisation du type existentiel pour les types de retour ?

Oui, la position de retour impl Trait dans les traits a été reportée jusqu'à ce que nous ayons une expérience plus pratique de l'utilisation des types existentiels dans les traits.

@cramertj Sommes-nous encore au point où nous avons suffisamment d'expérience pratique pour mettre cela en œuvre ?

J'aimerais voir impl Trait dans quelques versions stables avant d'ajouter plus de fonctionnalités.

@mark-im Je ne vois pas ce qui est controversé à propos de la position impl Trait retour

Je ne pense pas que ce soit controversé. J'ai juste l'impression que nous ajoutons des fonctionnalités trop rapidement. Ce serait bien de s'arrêter et de se concentrer sur la dette technique pendant un certain temps et d'acquérir d'abord de l'expérience avec l'ensemble de fonctionnalités actuel.

Je vois. Je suppose que je le considère simplement comme une partie manquante d'une fonctionnalité existante plus qu'une nouvelle fonctionnalité.

Je pense que @alexreg a raison, il est très tentant d'utiliser des méthodes existentielles impl Trait sur les traits. Ce n'est pas vraiment une nouvelle fonctionnalité mais je suppose qu'il y a quelques points à régler avant d'essayer de l'implémenter ?

@phaazon Peut-être, oui... Je ne sais pas vraiment à quel point les détails de mise en œuvre différeraient par rapport à ce que nous avons déjà aujourd'hui, mais peut-être que quelqu'un pourrait commenter cela. J'aimerais aussi voir des types existentiels pour les liaisons let/const, mais je peux certainement accepter cela comme une fonctionnalité au-delà de celle-ci, attendant ainsi un autre cycle avant de commencer.

Je me demande si on peut retenir l'impl universel Trait in traits...

Mais oui, je suppose que je vois votre point.

@mark-im Non, nous ne pouvons pas, ils sont déjà stables .

Ils sont dans des fonctions, mais qu'en est-il des déclarations de Trait ?

@mark-im comme le montre l'extrait, ils sont stables à la fois dans les déclarations d'impls et de traits.

Juste sauter pour rattraper où nous nous installons avec abstract type . Personnellement, je suis à peu près d' accord avec la syntaxe et les meilleures pratiques récemment proposées par

// GOOD:
type Foo: Iterator<Item: Display>;

type Bar = (impl Display, impl Debug);

// BAD
type Foo = impl Iterator<Item = impl Display>;

type Bar0: Display;
type Bar1: Debug;
type Bar = (Bar0, Bar1);

Ce qui s'appliquait à certains de mes codes, je suppose que cela ressemblerait à quelque chose comme:

// Concrete type with a generic body
struct Data<TBody> {
    ts: Timestamp,
    body: TBody,
}


// A name for an inferred iterator
type IterData = Data<impl Read>;
type Iter: Iterator<Item = IterData>;


// A function that gives us an iterator. Also takes some arbitrary range
fn iter(&self, range: impl RangeBounds<Timestamp>) -> Result<Iter, Error> { ... }


// A struct that holds on to that iterator
struct HoldsIter {
    iter: Iter,
}

Cela n'a pas de sens pour moi que type Bar = (impl Display,); serait bien, mais type Bar = impl Display; serait mauvais.

Si nous décidons de différentes syntaxes de type existentiel alternatives (toutes différentes de la rfc 2071 ?), un fil de discussion sur https://users.rust-lang.org/ serait-il un bon endroit pour le faire ?

Je n'ai pas assez de compréhension des alternatives pour démarrer un tel fil maintenant, mais comme les types existentiels ne sont toujours pas implémentés, je pense qu'une discussion sur les forums puis une nouvelle RFC serait probablement mieux que d'en parler dans le problème de suivi .

Quel est le problème avec type Foo = impl Trait ?

@daboross Probablement le forum des internes à la place. J'envisage d'écrire une RFC à ce sujet pour finaliser la syntaxe.

@daboross Il y a déjà eu plus qu'assez de discussions sur la syntaxe sur ce fil. Je pense que si @Centril peut rédiger une RFC à ce stade, alors tant

Y a-t-il un problème auquel je peux m'abonner pour discuter des existentiels dans les traits ?

Existe-t-il un argument lié aux macros pour une syntaxe ou l'autre ?

@tomaka dans le premier cas, le type Foo = (impl Display,) est vraiment la seule syntaxe que vous avez. Ma préférence pour type Foo: Trait rapport à type Foo = impl Trait vient simplement du fait que nous lions un type que nous pouvons nommer, comme <TFoo: Trait> ou where TFoo: Trait , alors qu'avec impl Trait nous ne pouvons pas nommer le type.

Pour clarifier, je ne dis pas que type Foo = impl Bar est mauvais, je dis que type Foo: Bar est meilleur dans des cas simples, en partie grâce à la motivation de @KodrAus .

Je lis ce dernier comme : "le type Foo satisfait Bar" et le premier comme : "le type Foo est égal à un type qui satisfait Bar". Le premier est donc, à mon avis, plus direct et naturel d'un point de vue extensionnel ("ce que je peux faire avec Foo"). Pour comprendre ce dernier, vous devez impliquer une compréhension plus profonde de la quantification existentielle des types.

type Foo: Bar est également très pratique car si c'est la syntaxe utilisée comme limite sur un type associé dans un trait, alors vous pouvez simplement copier la déclaration dans le trait dans l'impl et cela fonctionnera simplement (si c'est toutes les informations que vous souhaitez exposer..).

La syntaxe est également plus rapide, en particulier lorsque des limites de type associées sont impliquées et lorsqu'il existe de nombreux types associés. Cela peut réduire le bruit et donc faciliter la lisibilité.

@KodrAus

Voici comment je lis ces définitions de type :

  • type Foo: Trait signifie " Foo est un type implémentant Trait "
  • type Foo = impl Trait signifie " Foo est un alias d'un certain type implémentant Trait "

Pour moi, Foo: Trait déclare simplement une contrainte sur Foo implémentant Trait . D'une certaine manière, type Foo: Trait semble incomplet. Il semble que nous ayons une contrainte, mais la définition réelle de Foo est manquante.

D'un autre côté, impl Trait évoque "c'est un type unique, mais le compilateur trouve son nom". Par conséquent, type Foo = impl Trait implique que nous avons déjà un type concret (qui implémente Trait ), dont Foo n'est qu'un alias.

Je pense que type Foo = impl Trait exprime plus clairement le sens correct : Foo est un alias d'un certain type implémentant Trait .

@stjepang

type Foo: Trait signifie "Foo est un type mettant en œuvre le trait"
[..]
D'une certaine manière, type Foo: Trait semble incomplet.

C'est aussi comme ça que je l'ai lu (phrasé modulo...), et c'est une interprétation extensionnellement correcte. Cela dit tout sur ce que vous pouvez faire avec Foo (les morphismes que le type permet). Par conséquent, il est extensionnellement complet. Du point de vue des lecteurs et en particulier du point de vue des débutants, je pense que l'extensionnalité est plus importante.

D'un autre côté, impl Trait évoque "c'est un type unique, mais le compilateur comble le vide". Par conséquent, type Foo = impl Trait implique que nous avons déjà un type concret (qui implémente Trait ), dont Foo est un alias, mais le compilateur déterminera de quel type il s'agit vraiment.

Il s'agit d'une interprétation plus détaillée et intensionnelle concernant la représentation qui est redondante d'un point de vue extensionnel. Mais ceci est plus complet dans un sens intensionnel.

@Centril

Du point de vue des lecteurs et en particulier du point de vue des débutants, je pense que l'extensionnalité est plus importante.

Il s'agit d'une interprétation plus détaillée et intensionnelle concernant la représentation qui est redondante d'un point de vue extensionnel

La dichotomie extensionnelle vs intensionnelle est intéressante - je n'avais jamais pensé à impl Trait cette façon auparavant.

Pourtant, je prie de différer sur la conclusion. FWIW, je n'ai jamais réussi à trouver des types existentiels en Haskell et Scala, alors comptez-moi comme un débutant. :) impl Trait dans Rust m'a semblé très intuitif dès le premier jour, ce qui est probablement dû au fait que je le considère comme un alias restreint plutôt que comme ce qui peut être fait avec le type. Donc , entre savoir ce que Foo et ce qui peut être fait avec elle, je prends l'ancien.

Juste mon 2c, cependant. D'autres peuvent avoir des modèles mentaux différents de impl Trait .

Je suis entièrement d'accord avec ce commentaire : type Foo: Trait me semble incomplet. Et type Foo = impl Trait semble plus similaire aux utilisations de impl Trait ailleurs, ce qui aide la langue à se sentir plus cohérente et mémorable.

@joshtriplett Voir https://github.com/rust-lang/rust/issues/34511#issuecomment -387238653 pour commencer la discussion sur la cohérence ; Je crois qu'autoriser les formulaires est en fait la chose cohérente à faire. Et n'autoriser qu'une des formes (celle-ci...) est incohérente. Autoriser type Foo: Trait s'accorde également particulièrement bien avec https://github.com/rust-lang/rfcs/pull/2289 avec lequel vous pouvez déclarer : type Foo: Iterator<Item: Display>; ce qui rend les choses parfaitement uniformes.

@stjepang La perspective extensionnelle de type Foo: Bar; ne vous oblige pas à comprendre la quantification existentielle en théorie des types. Tout ce que vous devez vraiment comprendre, c'est que Foo vous permet de faire toutes les opérations offertes par Bar , c'est tout. Du point de vue d'un utilisateur du point Foo de

@Centril

Je crois que je comprends maintenant d'où vous venez et l'intérêt de pousser la syntaxe Type: Trait dans autant d'endroits que possible.

Il y a une forte connotation autour de : utilisé pour les limites de type-implements-trait et = pour les définitions de type et les limites de type-égal-autre-type.

Je pense que cela apparaît également dans votre RFC. Par exemple, prenons ces deux types de bornes :

  • Foo: Iterator<Item: Bar>
  • Foo: Iterator<Item = impl Bar>

Ces deux bornes ont finalement le même effet, mais sont (je pense) subtilement différentes. Le premier dit " Item doit implémenter le trait Bar ", tandis que le dernier dit " Item doit être égal à un type implémentant Bar ".

Laissez-moi essayer d'illustrer cette idée en utilisant un autre exemple :

trait Person {
    type Name: Into<String>; // Just a type bound, not a definition!
    // ...
}

struct Alice;

impl Person for Alice {
    type Name = impl Into<String>; // A concrete type definition.
    // ...
}

Comment devrions-nous définir un type existentiel qui implémente alors Person ?

  • type Someone: Person , qui ressemble à un type lié.
  • type Someone = impl Person , qui ressemble à une définition de type.

@stjepang Ressembler à un type lié n'est pas une mauvaise chose :) Nous pouvons implémenter Person for Alice comme ceci :

struct Alice;
trait Person          { type Name: Into<String>; ... }
impl Person for Alice { type Name: Into<String>; ... }

Regarde moi ! Les éléments à l'intérieur de { .. } pour le trait et l'impl sont identiques, ce qui signifie que vous pouvez copier le texte du trait intact en ce qui concerne Name .

Comme un type associé est une fonction de niveau de type (où le premier argument est Self ), nous pouvons voir un alias de type comme un type associé d'arité 0, donc rien d'étrange ne se passe.

Ces deux bornes ont finalement le même effet, mais sont (je pense) subtilement différentes. Le premier dit « L'élément doit implémenter le trait Bar », tandis que le second dit « L'élément doit être égal à un type mettant en œuvre la barre ».

Ouais; Je trouve le premier phrasé plus pertinent et naturel. :)

@Centril Hé. Cela signifie-t-il que type Thing; est suffisant pour introduire un type abstrait ?

trait Neg           { type Output; fn neg(self) -> Self::Output; }
impl Neg for MyType { type Output; fn neg(self) -> Self::Output { self } }

@kennytm Je pense que c'est techniquement possible ; mais vous pourriez demander si c'est souhaitable ou non en fonction de vos réflexions sur l'implicite/explicite. Dans ce cas particulier, je pense qu'il serait techniquement suffisant d'écrire :

trait Neg           { type Output; fn neg(self) -> Self::Output; }
impl Neg for MyType { fn neg(self) -> Self::Output { self } }

et le compilateur pourrait simplement déduire type Output: Sized; pour vous (ce qui est une limite profondément inintéressante qui ne vous donne aucune information). C'est quelque chose à considérer pour des limites plus intéressantes, mais ce ne sera pas dans ma proposition initiale car je pense que cela pourrait encourager les API à faible accessibilité, même lorsque le type concret est très simple, en raison de la paresse du programmeur :) type Output; être initialement pour la même raison.

Je pense qu'après avoir lu tout cela, j'ai tendance à être plus d'accord avec @Centril. Quand je vois type Foo = impl Bar j'ai tendance à penser que Foo est un type particulier, comme avec les autres alias. Mais ce n'est pas. Considérez cet exemple :

type Displayable = impl Display;

fn foo() -> Displayable { "hi" }
fn bar() -> Displayable { 42 }

À mon humble avis, c'est un peu bizarre de voir = dans la déclaration de Displayable mais de ne pas avoir les types de retour de foo et bar égaux (c'est-à-dire que ce = n'est pas transitif, contrairement à partout ailleurs). Le problème est que Foo n'est _pas_ un alias pour un type particulier qui implémente un trait. En d'autres termes, il s'agit d'un type unique quel que soit le contexte dans lequel il est utilisé, mais ce type peut être différent pour différentes utilisations, comme dans l'exemple.

Quelques personnes ont mentionné que type Foo: Bar se sentait « incomplet ». Pour moi, c'est une bonne chose. Dans un certain sens, Foo est incomplet ; nous ne savons pas ce que c'est, mais nous savons que cela satisfait Bar .

@mark-im

Le problème est que Foo n'est pas un alias pour un type particulier qui implique un trait. En d'autres termes, il s'agit d'un type unique quel que soit le contexte dans lequel il est utilisé, mais ce type peut être différent pour différentes utilisations, comme dans l'exemple.

Wow, est-ce vraiment vrai ? Ce serait certainement très déroutant pour moi.

Y a-t-il une raison pour laquelle Displayable serait un raccourci pour impl Display plutôt qu'un seul type concret ? Un tel comportement est-il même utile étant donné que les alias de traits (problème de suivi : https://github.com/rust-lang/rust/issues/41517) peuvent être utilisés de la même manière ? Exemple:

trait Displayable = Display;

fn foo() -> impl Displayable { "hi" }
fn bar() -> impl Displayable { 42 }

@mark-im

type Displayable = impl Display;

fn foo() -> Displayable { "hi" }
fn bar() -> Displayable { 42 }

Ce n'est pas un exemple valable. De la section de référence sur les types existentiels dans RFC 2071 :

existential type Foo = impl Debug;

Foo peut être utilisé comme i32 à plusieurs endroits dans le module. Cependant, chaque fonction qui utilise Foo comme i32 doit placer indépendamment des contraintes sur Foo telles qu'il doit être i32

Chaque déclaration de type existentiel doit être contrainte par au moins un corps de fonction ou un initialiseur const/static. Un corps ou un initialiseur doit soit contraindre totalement, soit n'imposer aucune contrainte à un type existentiel donné.

Pas directement mentionné, mais requis pour que le reste de la RFC fonctionne, est que deux fonctions dans la portée du type existentiel ne peuvent pas déterminer un type concret différent pour cet existentiel. Ce sera une forme d'erreur de type conflictuel.

Je suppose que votre exemple donnerait quelque chose comme expected type `&'static str` but found type `i32` sur le retour de bar , puisque foo aurait déjà défini le type concret de Displayable à &'static str .

EDIT : À moins que vous ne veniez à cela par intuition que

type Displayable = impl Display;

fn foo() -> Displayable { "hi" }
fn bar() -> Displayable { 42 }

est équivalent à

fn foo() -> impl Display { "hi" }
fn bar() -> impl Display { 42 }

plutôt que mon attente de

existential type _0 = impl Display;
type Displayable = _0;

fn foo() -> Displayable { "hi" }
fn bar() -> Displayable { 42 }

Je suppose que laquelle de ces deux interprétations est correcte pourrait dépendre de la RFC que @Centril peut écrire.

Le problème est que Foo n'est pas un alias pour un type particulier qui implique un trait.

Je suppose que laquelle de ces deux interprétations est correcte pourrait dépendre de la RFC que @Centril peut écrire.

La raison pour laquelle type Displayable = impl Display; existe est qu'il s'agit d'un alias pour un type particulier.
Voir https://github.com/rust-lang/rfcs/issues/1738 , qui est le problème que cette fonctionnalité résout.

@ Nemo157 Votre attente est correcte. :)

Ce qui suit:

type Foo = (impl Bar, impl Bar);
type Baz = impl Bar;

serait désudé pour :

/* existential */ type _0: Bar;
/* existential */ type _1: Bar;
type Foo = (_0, _1);

/* existential */ type _2: Bar;
type Baz = _2;

_0 , _1 et _2 sont tous des types nominalement différents donc Id<_0, _1> , Id<_0, _2> , Id<_1, _2> (et le instances symétriques) sont toutes inhabitées, où Id est défini dans refl .

Avis de non-responsabilité : je n'ai (volontairement) pas lu la RFC (mais je sais de quoi il s'agit), afin de pouvoir commenter ce qui semble « intuitif » avec les syntaxes.

Pour la syntaxe type Foo: Trait , je m'attendrais complètement à ce que quelque chose comme ceci soit possible :

trait Trait {
    type Foo: Display;
    type Foo: Debug;
}

De la même manière que where Foo: Display, Foo: Debug est actuellement possible.

Si la syntaxe n'est pas autorisée, je pense que c'est un problème avec la syntaxe.

Oh, et je pense que plus Rust a de syntaxe, plus il devient difficile de l'apprendre. Même si une syntaxe est «plus facile à apprendre», tant que les deux syntaxes sont nécessaires, le débutant devra éventuellement apprendre les deux, et probablement le plus tôt possible s'il se lance dans un projet déjà existant.

@Ekleog

Pour la syntaxe type Foo: Trait , je m'attendrais complètement à ce que quelque chose comme ceci soit possible :

C'est possible. Ces "alias de type" déclarent des types associés (les alias de type peuvent être interprétés comme des fonctions de niveau de type 0-aire tandis que les types associés sont des fonctions de niveau de type 1+-aire). Bien sûr, vous ne pouvez pas avoir plusieurs types associés avec le même nom dans un même trait, ce serait comme essayer de définir deux alias de type avec le même nom dans un module. Dans un impl , type Foo: Bar correspond aussi à la quantification existentielle.

Oh, et je pense que plus Rust a de syntaxe, plus il devient difficile de l'apprendre.

Les deux syntaxes sont déjà utilisées. type Foo: Bar; est déjà légal dans les traits, et aussi pour la quantification universelle comme Foo: BarFoo est une variable de type. impl Trait est utilisé pour la quantification existentielle en position de retour et pour la quantification universelle en position d'argument. Permettre à la fois de combler les lacunes de cohérence dans la langue. Ils sont également optimaux pour différents scénarios, et donc le fait d'avoir les deux vous donne l'optimum global.

De plus, il est peu probable que le débutant ait besoin de type Foo = (impl Bar, impl Baz); . La plupart des utilisations seront probablement type Foo: Bar; .

La demande d'extraction originale pour la RFC 2071 mentionne un mot-clé typeof qui semble avoir été entièrement rejeté dans cette discussion. Je trouve que la syntaxe actuellement proposée est plutôt implicite, car le compilateur et tout humain qui lit le code recherche le type concret.

Je préférerais que cela soit rendu explicite. Alors au lieu de

type Foo = impl SomeTrait;
fn foo_func() -> Foo { ... }

nous écririons

fn foo_func() -> impl SomeTrait { ... }
type Foo = return_type_of(foo_func);

(avec le nom du return_type_of à bikeshedded), ou même

fn foo_func() -> impl SomeTrait as Foo { ... }

qui n'aurait même pas besoin de nouveaux mots-clés et est facilement compréhensible par quiconque connaît la syntaxe impl Trait. Cette dernière syntaxe est concise et regroupe toutes les informations au même endroit. Pour les traits, cela pourrait ressembler à ceci :

trait Bar
{
    type Assoc: SomeTrait;
    fn func() -> Assoc;
}

impl Bar for SomeType
{
    type Assoc = return_type_of(Self::func);
    fn func() -> Assoc { ... }
}

ou même

impl Bar for SomeType
{
    fn func() -> impl SomeTrait as Self::Assoc { ... }
}

Je suis désolé si cela a déjà été discuté et rejeté, mais je n'ai pas pu le trouver.

@Centril

C'est possible. Ces "alias de type" déclarent des types associés (les alias de type peuvent être interprétés comme des fonctions de niveau de type 0-aire tandis que les types associés sont des fonctions de niveau de type 1+-aire). Bien sûr, vous ne pouvez pas avoir plusieurs types associés avec le même nom dans un même trait, ce serait comme essayer de définir deux alias de type avec le même nom dans un module. Dans un impl, tapez Foo : Bar correspond également à la quantification existentielle.

(désolé, je voulais le mettre dans un impl Trait for Struct , pas dans un trait Trait )

Je suis désolé, je ne suis pas sûr de comprendre. Ce que j'essaie de dire, c'est pour moi un code comme

impl Trait for Struct {
    type Type: Debug;
    type Type: Display;

    fn foo() -> Self::Type { 42 }
}

(lien aire de jeux pour la version complète)
l'impression que cela devrait fonctionner.

Parce que c'est juste mettre deux limites sur Type , de la même manière que where Type: Debug, Type: Display work .

Si cela ne doit pas être autorisé (ce que je semble comprendre par « Bien sûr, vous ne pouvez pas avoir plusieurs types associés avec le même nom dans un seul trait » ? mais étant donné mon erreur d'écriture trait Trait au lieu de impl Trait for Struct Je ne suis pas sûr), alors je pense que c'est un problème avec la syntaxe type Type: Trait .

Ensuite, à l'intérieur d'une déclaration trait , la syntaxe est déjà type Type: Trait , et ne permet pas plusieurs définitions. Donc je suppose que peut-être ce bateau a déjà navigué il y a longtemps…

Cependant, comme indiqué ci-dessus par @stjepang et @joshtriplett , type Type: Trait semble incomplet. Et bien que cela puisse avoir du sens dans les déclarations trait (il est en fait conçu pour être incomplet, même si c'est étrange qu'il n'autorise pas plusieurs définitions), cela n'a pas de sens dans un bloc impl Trait , où le type est censé être connu avec certitude (et ne peut actuellement être écrit que sous la forme type Type = RealType )

impl Trait est utilisé pour la quantification existentielle en position retour et pour la quantification universelle en position argument.

Oui, j'ai aussi pensé à impl Trait en position d'argument en écrivant ceci, et je me suis demandé si je devais dire que j'aurais soutenu le même argument pour impl Trait en position d'argument si j'avais su qu'il était en cours de stabilisation . Cela dit je pense qu'il vaudrait mieux ne pas relancer ce débat :)

Permettre à la fois de combler les lacunes de cohérence dans la langue. Ils sont également optimaux pour différents scénarios, et donc le fait d'avoir les deux vous donne l'optimum global.

Optimal et simplicité

Eh bien, je pense que parfois perdre l'optimum au profit de la simplicité est une bonne chose. Comme, C et ML sont nés à peu près à la même époque. C a fait d' énormes concessions à l'optimum en faveur de la simplicité, ML était beaucoup plus proche de l'optimum mais beaucoup plus complexe. Même en comptant les dérivés de ces langages, je ne pense pas que le nombre de développeurs C et de développeurs ML soit comparable.

impl Trait et :

Actuellement, autour des syntaxes impl Trait et : , j'ai l'impression qu'il y a une tendance à créer deux syntaxes alternatives pour le même ensemble de fonctionnalités. Cependant, je ne pense pas que ce soit une bonne chose, car avoir deux syntaxes pour les mêmes fonctionnalités ne peut que dérouter les utilisateurs, surtout quand ils différeront toujours subtilement dans leur sémantique exacte.

Imaginez un débutant qui a toujours vu type Type: Trait venir sur son premier type Type = impl Trait . Ils peuvent probablement deviner ce qui se passe, mais je suis presque sûr qu'il y aura un moment de « WTF, c'est ça ? J'utilise Rust depuis des années et il y a toujours une syntaxe que je n'ai jamais vue ?”. C'est plus ou moins le piège dans lequel le C++ est tombé.

Caractéristique ballonnement

Ce que je pense, c'est que plus il a de fonctionnalités, plus la langue est difficile à apprendre. Et je ne vois pas d'énorme avantage à utiliser type Type: Trait rapport à type Type = impl Trait : c'est, genre, 6 caractères sauvegardés ?

Avoir rustc afficher une erreur en voyant type Type: Trait qui dit que la personne qui l'écrit pour utiliser type Type = impl Trait aurait beaucoup plus de sens pour moi: au moins il y a une seule façon d'écrire les choses , cela a du sens pour tous ( impl Trait est déjà clairement reconnu comme une position existentielle en retour), et cela couvre tous les cas d'utilisation. Et si les gens essaient d'utiliser ce qu'ils pensent être intuitif (bien que je ne sois pas d'accord avec cela, pour moi = impl Trait est plus intuitif, par rapport au = i32 actuel), ils sont légitimement redirigés vers le manière conventionnellement correcte de l'écrire.

La demande d'extraction originale pour la RFC 2071 mentionne un type de mot-clé qui semble avoir été entièrement rejeté dans cette discussion. Je trouve que la syntaxe actuellement proposée est plutôt implicite, car le compilateur et tout humain qui lit le code recherche le type concret.

typeof été brièvement discuté dans le numéro que j'ai ouvert il y a 1,5 ans : https://github.com/rust-lang/rfcs/issues/1738#issuecomment -258353755

En tant que débutant, je trouve la syntaxe type Foo: Bar confuse. C'est la syntaxe de type associée, mais ceux-ci sont censés être dans des traits, pas des structs. Si vous voyez impl Trait une fois, vous pouvez comprendre ce que c'est, ou sinon vous pouvez le rechercher. C'est plus difficile à faire avec l'autre syntaxe, et je ne suis pas sûr de l'avantage.

Il semble que certaines personnes de l'équipe linguistique sont vraiment opposées à l'utilisation de impl Trait pour nommer les types existentiels, elles préfèrent donc utiliser autre chose à la place. Même le commentaire ici n'a pas de sens pour moi.

Mais de toute façon, je pense que ce cheval a été battu à mort. Il y a probablement des centaines de commentaires sur la syntaxe, et seulement une poignée de suggestions (je me rends compte que je ne fais qu'empirer les choses). Il est clair qu'aucune syntaxe ne rendra tout le monde heureux, et il existe des arguments pour et contre chacun d'eux. Peut-être devrions-nous en choisir un et nous en tenir à lui.

Ouah, ce n'est pas du tout ce que j'ai compris. Merci @Nemo157 de m'avoir mis au clair !

Dans ce cas, je préférerais en effet la syntaxe =.

@Ekleog

alors je pense que c'est un problème avec la syntaxe type Type: Trait .

Cela pourrait être autorisé et ce serait parfaitement bien défini, mais vous écrivez généralement where Type: Foo + Bar au lieu de where Type: Foo, Type: Bar , donc cela ne semble pas être une très bonne idée. Vous pouvez également facilement déclencher un bon message d'erreur pour ce cas suggérant d'écrire Foo + Bar place dans le cas du type associé.

type Foo = impl Bar; également des problèmes de compréhension en ce sens que vous voyez = impl Bar et concluez que vous pouvez simplement le remplacer à chaque occurrence où il est utilisé comme -> impl Bar ; mais cela ne fonctionnerait pas. @mark-im a fait cette interprétation, ce qui semble être une erreur beaucoup plus probable. Par conséquent, je conclus que type Foo: Bar; est le meilleur choix pour l'apprentissage.

Cependant, comme indiqué ci-dessus par @stjepang et @joshtriplett , tapez Type: Trait semble incomplet.

Il n'est pas incomplet à partir d'un POV extensionnel. Vous obtenez précisément autant d'informations de type Foo: Bar; que de type Foo = impl Bar; . Donc du point de vue de ce que vous pouvez faire avec type Foo: Bar; , c'est complet. En fait, ce dernier est désucré comme type _0: Bar; type Foo = _0; .

EDIT : ce que je voulais dire, c'est que même si cela peut sembler incomplet pour certains, ce n'est pas d'un point de vue technique.

Cela dit je pense qu'il vaudrait mieux ne pas relancer ce débat :)

C'est une bonne idée. Nous devons considérer le langage tel qu'il est lors de la conception, et non tel que nous l'aurions souhaité.

Eh bien, je pense que parfois perdre l'optimum au profit de la simplicité est une bonne chose.

Si nous devions privilégier la simplicité, je laisserais plutôt type Foo = impl Bar; place.
Il convient de noter que la simplicité supposée de C (supposée, car Haskell Core et des choses similaires sont probablement plus simples tout en restant solides..) a un prix élevé en termes d'expressivité et de solidité. C n'est pas mon étoile du nord dans la conception de langage ; loin de là.

Actuellement, autour des syntaxes impl Trait et : , j'ai l'impression qu'il y a une tendance à créer les deux syntaxes alternatives pour le même ensemble de fonctionnalités. Cependant, je ne pense pas que ce soit une bonne chose, car avoir deux syntaxes pour les mêmes fonctionnalités ne peut que dérouter les utilisateurs, surtout quand ils différeront toujours subtilement dans leur sémantique exacte.

Mais ils ne différeront
Je pense que la confusion d'essayer d'écrire type Foo: Bar; ou type Foo = impl Bar uniquement pour que l'un d'entre eux ne fonctionne pas même si les deux ont une sémantique parfaitement bien définie ne concerne que l'utilisateur. Si un utilisateur essaie d'écrire type Foo = impl Bar; , alors une peluche se déclenche et propose type Foo: Bar; . Le lint enseigne à l'utilisateur l'autre syntaxe.
Pour moi, il est important que la langue soit uniforme et cohérente ; Si nous avons décidé d'utiliser les deux syntaxes quelque part, nous devons appliquer cette décision de manière cohérente.

Imaginez un débutant qui a toujours vu type Type: Trait venir sur son premier type Type = impl Trait .

Dans ce cas spécifique, un lint se déclencherait et recommanderait l'ancienne syntaxe. En ce qui concerne type Foo = (impl Bar, impl Baz); , le débutant devra apprendre -> impl Trait dans tous les cas, il devrait donc être capable d'en déduire le sens.

C'est plus ou moins le piège dans lequel le C++ est tombé.

Le problème de C++ est principalement qu'il est assez ancien, qu'il a le bagage de C et de nombreuses fonctionnalités prenant en charge trop de paradigmes. Ce ne sont pas des caractéristiques distinctes, juste une syntaxe différente.

Ce que je pense, c'est que plus il a de fonctionnalités, plus la langue est difficile à apprendre.

Je pense qu'apprendre une nouvelle langue consiste principalement à apprendre ses importantes bibliothèques. C'est là que le plus de temps sera passé. Les bonnes fonctionnalités peuvent rendre les bibliothèques beaucoup plus composables et fonctionner dans plus de cas. Je préfère de loin un langage qui donne un bon pouvoir d'abstraction à celui qui oblige à penser bas niveau et qui provoque des doublons. Dans ce cas, nous n'ajoutons pas plus de puissance abstraite ou même pas vraiment de fonctionnalités, juste une meilleure ergonomie.

Et je ne vois pas un énorme avantage à utiliser type Type: Trait sur type Type = impl Trait: c'est, genre, 6 caractères enregistrés ?

Oui, seulement 6 caractères enregistrés. Mais si nous considérons type Foo: Iterator<Item: Iterator<Item: Display>>; , alors nous obtiendrions à la place : type Foo = impl Iterator<Item = impl Iterator<Item = impl Display>>; qui a beaucoup plus de bruit. type Foo: Bar; est également plus direct par rapport à ce dernier, moins sujet aux erreurs d'interprétation (re. substitution..), et fonctionne mieux pour les types associés (copiez le type du trait..).
De plus, type Foo: Bar pourrait être naturellement étendu à type Foo: Bar = ConcreteType; ce qui exposerait le type concret mais garantirait également qu'il satisfait Bar . Rien de tel ne peut être fait pour type Foo = impl Trait; .

Le fait que rustc affiche une erreur en voyant type Type: Trait qui dit que la personne qui l'écrit pour utiliser type Type = impl Trait aurait beaucoup plus de sens pour moi : au moins il y a une seule façon d'écrire les choses,

ils sont à juste titre redirigés vers la manière conventionnellement correcte de l'écrire.

Je propose qu'il y ait une manière conventionnelle d'écrire les choses ; type Foo: Bar; .

@lnicola

En tant que débutant, je trouve la syntaxe type Foo: Bar confuse. C'est la syntaxe de type associée, mais ceux-ci sont censés être dans des traits, pas des structs.

Je répète que les alias de type peuvent vraiment être considérés comme des types associés. Vous pourrez dire :

trait Foo        { type Baz: Quux; }
// User of `Bar::Baz` can conclude `Quux` but nothing more!
impl Foo for Bar { type Baz: Quux; }

// User of `Wibble` can conclude `Quux` but nothing more!
type Wibble: Quux;

Nous voyons que cela fonctionne exactement de la même manière dans les types et alias de types associés.

Oui, seulement 6 caractères enregistrés. Mais si nous considérons type Foo: Iterator<Item: Iterator<Item: Display>>; , alors nous obtiendrons à la place : type Foo = impl Iterator<Item = impl Iterator<Item = impl Display>> ; qui a beaucoup plus de bruit.

Cela semble orthogonal à la syntaxe pour déclarer un existentiel nommé. Les quatre syntaxes dont je me souviens avoir été proposées le permettraient toutes potentiellement car

type Foo: Iterator<Item: Iterator<Item: Display>>;
type Foo = impl Iterator<Item: Iterator<Item: Display>>;
existential type Foo: Iterator<Item: Iterator<Item: Display>>;
existential type Foo = impl Iterator<Item: Iterator<Item: Display>>;

Pouvoir utiliser votre raccourci proposé Trait<AssociatedType: Bound> au lieu de la syntaxe Trait<AssociatedType = impl Bound> pour déclarer des types existentiels anonymes pour les types associés d'un type existentiel (nommé ou anonyme) est une fonctionnalité indépendante (mais probablement pertinente dans termes de maintien de la cohérence de l'ensemble des caractéristiques de type existentiel).

@ Nemo157 Ce sont des fonctionnalités différentes, oui; mais je pense qu'il est naturel de les considérer ensemble par souci de cohérence.

@Centril

Je suis désolé, mais ils ont tort. Il n'est pas incomplet à partir d'un POV extensionnel.

Je n'ai jamais suggéré que votre syntaxe proposée manquait d'informations ; Je suggérais qu'il se sent incomplet; cela me semble mal, et pour les autres. Je comprends que vous n'êtes pas d'accord avec cela, du point de vue d'où vous venez, mais cela ne rend pas ce sentiment faux.

Notez également que dans ce fil, les gens ont démontré un problème d'interprétation avec cette différence de syntaxe exacte. type Foo = impl Trait impression qu'il est plus clair que Foo est un type concret spécifique mais sans nom, peu importe le nombre de fois que vous l'utilisez, plutôt qu'un alias pour un trait qui peut prendre un type concret différent chaque fois que vous l'utilisez.

Je pense que cela aide de dire aux gens qu'ils peuvent prendre toutes les choses qu'ils savent sur -> impl Trait et les appliquer à type Foo = impl Trait ; il y a un concept généralisé impl Trait qu'ils peuvent voir utilisé comme bloc de construction dans les deux endroits. Une syntaxe comme type Foo: Trait cache ce bloc de construction généralisé.

@joshtriplett

Je suggérais qu'il se sent incomplet; cela me semble mal, et pour les autres.

Bien; Je propose que nous utilisions ici un terme différent de incomplete car pour moi, cela suggère un manque d'information.

Notez également que dans ce fil, les gens ont démontré un problème d'interprétation avec cette différence de syntaxe exacte.

Ce que j'ai observé était une erreur d'interprétation, faite dans le fil, sur ce que signifie type Foo = impl Bar; . Une personne a interprété différentes utilisations de Foo comme n'étant pas nominalement le même type, mais plutôt des types différents. C'est-à-dire exactement : "un alias pour un trait qui peut prendre un type concret différent à chaque fois que vous l'utilisez" .

Certains ont déclaré que type Foo: Bar; est déroutant, mais je ne suis pas sûr de l'interprétation alternative de type Foo: Bar; qui est différente de la signification prévue. Je serais intéressé d'entendre parler d'interprétations alternatives.

@Centril

Je répète que les alias de type peuvent vraiment être considérés comme des types associés.

Ils le peuvent, mais pour le moment, les types associés sont liés aux traits. impl Trait fonctionne partout, ou presque. Si vous voulez présenter impl Trait comme une sorte de type associé, vous devrez introduire deux concepts à la fois. C'est-à-dire que vous voyez impl Trait comme type de retour de fonction, devinez ou lisez de quoi il s'agit, puis lorsque vous voyez impl Trait dans un alias de type, vous pouvez réutiliser cette connaissance.

Comparez cela à voir les types associés dans une définition de trait. Dans ce cas, vous pensez que c'est quelque chose que d'autres structures doivent définir ou implémenter. Mais si vous tombez sur un type Foo: Debug dehors d'un trait, vous ne saurez pas ce que c'est. Il n'y a personne pour le mettre en œuvre, alors s'agit-il d'une sorte de déclaration préalable ? Cela a-t-il quelque chose à voir avec l'héritage, comme c'est le cas en C++ ? Est-ce comme un module ML où quelqu'un d'autre choisit le type ? Et si vous avez déjà vu impl Trait , il n'y a rien pour faire un lien entre eux. Nous écrivons fn foo() -> impl ToString , pas fn foo(): ToString .

tapez Foo = impl Bar; a également des problèmes de compréhension dans la mesure où vous voyez = impl Bar et concluez que vous pouvez simplement le remplacer à chaque occurrence où il est utilisé comme -> impl Bar

Je l'ai déjà dit ici, mais c'est comme penser que let x = foo(); signifie que vous pouvez utiliser x au lieu de foo() . C'est en tout cas un détail que l'on peut consulter rapidement en cas de besoin, mais qui ne change pas fondamentalement le concept.

C'est-à-dire qu'il est facile de comprendre de quoi il s'agit (un type déduit comme dans -> impl Trait ), même si vous ne savez pas exactement comment cela fonctionne (ce qui se passe lorsque vous avez des définitions contradictoires). Avec l'autre syntaxe, il est même difficile de réaliser ce que c'est.

@Centril

Bien; Je propose que nous utilisions ici un terme différent d'incomplet car pour moi, cela suggère un manque d'information.

"incomplet" ne signifie pas nécessairement un manque d' informations , cela peut signifier que quelque chose semble être censé avoir quelque chose d'autre et n'en a pas.

type Foo: Trait; ne ressemble pas à une déclaration complète. On dirait qu'il manque quelque chose. Et cela semble gratuitement différent de type Foo = SomeType<X, Y, Z>; .

Peut-être que nous atteignons le point où nos one-liners à eux seuls ne peuvent pas vraiment combler cet écart de consensus entre type Inferred: Trait et type Inferred = impl Trait .

Pensez-vous qu'il vaudrait la peine de mettre en place une implémentation expérimentale de cette fonctionnalité avec n'importe quelle syntaxe (même celle spécifiée dans la RFC) afin que nous puissions commencer à jouer avec elle dans des programmes plus importants pour voir comment elle s'intègre dans le contexte ?

@lnicola

[..] impl Trait fonctionne partout, ou presque

Eh bien, Foo: Bound fonctionne aussi presque partout ;)

Mais si vous tombez sur un type Foo: Debug dehors d'un trait, vous ne saurez pas ce que c'est.

Je pense que la progression de son utilisation dans : trait -> impl -> type alias facilite l'apprentissage.
De plus, je pense que l'inférence que "le type Foo implémente Debug" est probablement de
voir type Foo: Debug dans les traits et à partir des limites génériques et c'est également correct.

Cela a-t-il quelque chose à voir avec l'héritage, comme c'est le cas en C++ ?

Je pense que l'absence d'héritage dans Rust doit être apprise à un stade beaucoup plus précoce que lors de l'apprentissage de la fonctionnalité dont nous discutons, car elle est si fondamentale pour Rust.

Est-ce comme un module ML où quelqu'un d'autre choisit le type ?

Cette inférence peut également être faite pour type Foo = impl Bar; raison de arg: impl Bar où l'appelant (utilisateur) choisit le type. Pour moi, l'inférence selon laquelle l'utilisateur choisit le type semble moins probable pour type Foo: Bar; .

Je l'ai déjà dit ici, mais c'est comme penser que let x = foo(); signifie que vous pouvez utiliser x au lieu de foo() .

Si le langage est référentiellement transparent, vous pouvez remplacer x par foo() . Jusqu'à ce que nous ajoutions type Foo = impl Foo; dans le système, les alias de type sont, pour ainsi dire, transparents référentiellement. Inversement, s'il existe déjà une liaison x = foo() disponible, alors les autres foo() in sont remplaçables par x .

@joshtriplett

« incomplet » ne signifie pas nécessairement un manque d'informations, cela peut signifier que quelque chose ressemble à quelque chose d'autre et n'en a pas.

Assez juste; mais qu'est-ce que c'est censé avoir qu'il n'y en a pas ?

type Foo: Trait; ne ressemble pas à une déclaration complète.

Ça m'a l'air complet. Cela ressemble à un jugement que Foo satisfait Trait qui est précisément le sens voulu.

@Centril pour moi, le "quelque chose qui manque" est le type réel pour

Je pense que nous sommes en quelque sorte en train d'épuiser ces arguments. Ce serait formidable de simplement implémenter les deux syntaxes expérimentalement et de voir ce qui fonctionne le mieux.

@mark-im

@Centril pour moi, le "quelque chose qui manque" est le type réel pour

C'est exactement ce que je ressens aussi.

Une chance de s'attaquer bientôt aux deux éléments différés, ainsi qu'au problème de l'élision à vie ? Je le ferais moi-même, mais je ne sais pas comment !

Il y a encore beaucoup de confusion autour de ce que signifie exactement impl Trait , et ce n'est pas du tout évident. Je pense que les éléments différés devraient certainement attendre jusqu'à ce que nous ayons une idée claire de la sémantique exacte de impl Trait (qui devrait arriver bientôt).

@varkor Quelle sémantique n'est pas claire ? AFAIK, rien sur la sémantique de impl Trait n'a changé depuis la RFC 1951 et s'est étendu en 2071.

@alexreg Je n'avais pas l'intention de le faire, mais voici un aperçu : après l'ajout de l'analyse, vous devez réduire les types de static s et const s à l'intérieur d'un impl existentiel contexte de trait, comme c'est fait ici pour les types de retour de fonctions. . Cependant, vous voudrez rendre le DefId dans ImplTraitContext::Existential facultatif, car vous ne voulez pas que votre impl Trait récupère les génériques d'une définition de fonction parent. Cela devrait vous faire avancer un peu. Vous aurez peut-être plus de facilité si vous vous basez sur le type PR existentiel de @oli-obk .

@cramertj : la sémantique de impl Trait dans le langage est entièrement restreinte à son utilisation dans les signatures de fonctions et il n'est pas vrai que l'étendre à d'autres positions ait un sens évident. Je dirai bientôt quelque chose de plus détaillé à ce sujet, où la plupart de la conversation semble se dérouler.

@varkor

la sémantique d'impl Trait dans le langage est entièrement limitée à son utilisation dans les signatures de fonctions et il n'est pas vrai que l'étendre à d'autres positions ait un sens évident.

La signification a été spécifiée dans la RFC 2071 .

@cramertj : la signification dans la RFC 2071 est ambiguë et permet de multiples interprétations de ce que signifie l'expression "type existentiel".

TL;DR — J'ai essayé de définir une signification précise pour impl Trait , qui, je pense, clarifie des détails qui, au moins intuitivement, n'étaient pas clairs ; ainsi qu'une proposition pour une nouvelle syntaxe d'alias de type.

Types existentiels dans Rust (post)


Il y a eu beaucoup de discussions dans le chat de Discord rust-lang sur la sémantique précise (c'est-à-dire formelle, théorique) de impl Trait au cours des deux derniers jours. Je pense qu'il a été utile de clarifier beaucoup de détails sur la fonctionnalité et ce qu'elle est et n'est pas exactement. Il met également en lumière les syntaxes plausibles pour les alias de type.

J'ai écrit un petit résumé de certaines de nos conclusions. Cela fournit une interprétation de impl Trait qui, à mon avis, est assez claire, et décrit précisément les différences entre la position d'argument impl Trait et la position impl Trait retour pas "universellement- quantifiés » vs « quantifiés existentiellement »). Il y a aussi quelques conclusions pratiques.

Dans celui-ci, je propose une nouvelle syntaxe répondant aux exigences communément énoncées d'un « alias de type existentiel » :
type Foo: Bar = _;

Parce que c'est un sujet tellement complexe, il y a beaucoup de choses qui doivent d'abord être clarifiées, donc je l'ai écrit dans un article séparé. Les commentaires sont très appréciés!

Types existentiels dans Rust (post)

@varkor

La RFC 2071 est ambiguë et permet de multiples interprétations de ce que l'expression "type existentiel" signifie ici.

En quoi est-ce ambigu? J'ai lu votre message - je ne connais toujours qu'un seul sens de l'existentiel non dynamique en statique et en constantes. Il se comporte de la même manière que la position de retour impl Trait , en introduisant une nouvelle définition de type existentiel par élément.

type Foo: Bar = _;

Nous avons discuté de cette syntaxe lors de la RFC 2071. Comme je l'ai dit là-bas, j'aime que cela démontre clairement que Foo est un type inféré unique et qu'il laisse de la place aux types non inférés qui restent existentiels en dehors du module actuel ( par exemple type Foo: Bar = u32; ). Je n'ai pas aimé deux aspects de celui-ci : (1) il n'a pas de mot-clé et est donc plus difficile à rechercher et (b) il a le même problème de verbosité par rapport à type Foo = impl Trait que la syntaxe abstract type Foo: Bar; a : type Foo = impl Iterator<Item = impl Display>; devient type Foo: Iterator<Item = MyDisplay> = _; type MyDisplay: Display = _; . Je ne pense pas que l'un ou l'autre de ces éléments soit décisif, mais ce n'est pas une victoire claire d'une manière ou d'une autre OMI.

@cramertj L'ambiguïté revient ici :

type Foo = impl Bar;
fn f() -> Foo { .. }
fn g() -> Foo { .. }

Si Foo était vraiment un alias de type pour un type existentiel, alors f et g prendraient en charge différents types de retour concrets. Plusieurs personnes ont instinctivement lu cette syntaxe de cette façon, et en fait, certains participants à la discussion sur la syntaxe RFC 2071 viennent juste de se rendre compte que ce n'est pas ainsi que la proposition fonctionne dans le cadre de la récente discussion Discord.

Le problème est que, surtout face à l'argument-position impl Trait , il n'est pas du tout clair où le quantificateur existentiel est censé aller. Pour les arguments, la portée est étroite ; pour la position de retour, il semble étroitement limité mais s'avère être plus large que cela; pour type Foo = impl Bar deux positions sont plausibles. La syntaxe basée sur _ s'oriente vers une interprétation qui n'implique même pas « existentielle », contournant parfaitement ce problème.

Si Foo était vraiment un alias de type pour un type existentiel

(c'est moi qui souligne). J'ai lu que « an » comme « un spécifique », ce qui signifie que f et g ne prendraient pas en charge différents types de retour concrets, car ils font référence au même type existentiel. J'ai toujours vu que type Foo = impl Bar; utilisait la même signification que let foo: impl Bar; , c'est-à-dire introduisant un nouveau type existentiel anonyme ; rendre votre exemple équivalent à

existential type _0: Bar;
type Foo = _0;
fn f() -> Foo { .. }
fn g() -> Foo { .. }

ce que j'espère est relativement sans ambiguïté.


Un problème est que la signification de " impl Trait dans les alias de type" n'a jamais été spécifiée dans une RFC. Il est brièvement mentionné dans la section « Alternatives » de la RFC 2071 , mais explicitement ignoré en raison de ces ambiguïtés pédagogiques inhérentes.

J'ai aussi l'impression d'avoir vu certains mentionner que les alias de type ne sont déjà pas référentiellement transparents. Je pense que c'était sur ur.rl.o, mais je n'ai pas pu trouver la discussion après quelques recherches.

@cramertj
Pour faire suite au point de @rpjohnst , il existe plusieurs interprétations de la sémantique de impl Trait , qui sont toutes cohérentes avec l'utilisation actuelle dans les signatures, mais ont des conséquences différentes lors de l'extension de impl Trait à d'autres emplacements (j'en connais 2 autres que celui décrit dans le post, mais qui ne sont pas tout à fait prêts pour la discussion). Et je ne pense pas qu'il soit vrai que l'interprétation du message soit nécessairement la plus évidente (je n'ai personnellement vu aucune explication similaire sur APIT et RTIP de ce point de vue).

Concernant le type Foo: Bar = _; , je pense qu'il faudrait peut-être en reparler — il n'y a pas de mal à revisiter de vieilles idées avec un regard neuf. Concernant vos problèmes avec :
(1) Il n'a pas de mot-clé, mais c'est la même syntaxe que l'inférence de type n'importe où. La recherche de documentation pour "underscore" / "underscore type" / etc. pourrait facilement fournir une page sur l'inférence de type.
(2) Oui, c'est vrai. Nous avons réfléchi à une solution à ce problème, qui, je pense, correspond bien à la notation de soulignement, qui, espérons-le, sera bientôt prête à être suggérée.

Comme @cramertj, je ne vois pas vraiment l'argument ici.

Je ne vois tout simplement pas l'ambiguïté fondamentale décrite par le message de @varkor . Je pense que nous avons toujours interprété "type existentiel" dans Rust comme "il existe un type _unique_ qui..." et non "il existe au moins un type qui..." parce que (comme le dit le post de @varkor ) le ce dernier équivaut à « types universels » et, par conséquent, l'expression « type existentiel » serait totalement inutile si nous avions l'intention de permettre cette interprétation. autant que je sache, chaque RFC sur le sujet a toujours supposé que les types universels et existentiels étaient deux choses distinctes. Je comprends que dans la théorie des types, c'est ce que cela signifie et que l'isomorphisme est très mathématiquement réel, mais pour moi, c'est juste un argument selon lequel nous avons mal utilisé la terminologie de la théorie des types et que nous devons choisir un autre jargon pour cela, pas un argument qui la sémantique prévue de impl Trait n'a toujours pas été claire et doit être repensée.

L'ambiguïté de portée décrite par @rpjohnst est un problème sérieux, mais chaque syntaxe proposée peut potentiellement être confondue avec les types alises ou les types associés. Laquelle de ces confusions est « pire » ou « la plus probable » est précisément le hangar à vélos sans fin que nous n’avons déjà pas réussi à résoudre après plusieurs centaines de commentaires. J'aime que type Foo: Bar = _; semble résoudre le problème de type Foo: Bar; d'avoir besoin d'une explosion de plusieurs déclarations pour déclarer n'importe quel existentiel légèrement non trivial, mais je ne pense pas que cela suffise pour vraiment changer le situation de « bikeshed sans fin ».

Ce dont je suis convaincu, c'est que quelle que soit la syntaxe avec laquelle nous nous retrouvons, il faut un mot-clé autre que type , car toutes les syntaxes "juste type " sont trop trompeuses. En fait, n'utilisez peut-être pas type dans la syntaxe _du tout_ donc il n'y a aucun moyen que quelqu'un puisse supposer qu'il s'agit de "un alias de type, mais plus existentiel en quelque sorte".

existential Foo = impl Trait;
fn f() -> Foo { .. }
fn g() -> Foo { .. }
existential Foo: Trait;
fn f() -> Foo { .. }
fn g() -> Foo { .. }



md5-b59626c5715ed89e0a93d9158c9c2535



existential Foo: Trait = _;
fn f() -> Foo { .. }
fn g() -> Foo { .. }

Il n'est pas évident pour moi que l'un de ces éléments _empêche_ complètement l'interprétation erronée selon laquelle f et g pourraient renvoyer deux types différents mettant en œuvre Trait , mais je soupçonne que c'est aussi proche de la prévention que nous pourrions éventuellement obtenir.

@Ixrec
L'expression « type existentiel » est problématique spécifiquement _à cause_ de l'ambiguïté de la portée. Je n'ai vu personne d'autre souligner que la portée est entièrement différente pour l'APIT et le RPIT. Cela signifie qu'une syntaxe comme type Foo = impl Bar , où impl Bar est un "type existentiel" est intrinsèquement ambiguë.

Oui, la terminologie de la théorie des types a été largement utilisée à mauvais escient. Mais il a été mal utilisé (ou du moins pas expliqué) dans la RFC - il y a donc une ambiguïté provenant de la RFC elle-même.

L'ambiguïté de portée décrite par @rpjohnst est un problème sérieux, mais chaque syntaxe proposée peut potentiellement être confondue avec les types alises ou les types associés. Laquelle de ces confusions est « pire » ou « la plus probable » est précisément le hangar à vélos sans fin que nous n’avons déjà pas réussi à résoudre après plusieurs centaines de commentaires.

Non, je ne pense pas que ce soit vrai. Il est possible de trouver une syntaxe cohérente qui n'ait pas cette confusion. Je me risquerais au bike-shedding parce que les deux propositions actuelles sont mauvaises, donc elles ne satisfont vraiment personne.

Ce dont je suis convaincu, c'est que quelle que soit la syntaxe avec laquelle nous nous retrouvons, il faut un mot-clé autre que type

Je ne pense pas non plus que ce soit nécessaire. Dans vos exemples, vous avez inventé une notation entièrement nouvelle, ce que vous voulez éviter dans la conception des langages dans la mesure du possible, sinon vous créez un énorme langage plein de syntaxe incohérente. Vous ne devriez explorer une syntaxe complètement nouvelle que lorsqu'il n'y a pas de meilleures options. Et je soutiens qu'il existe une meilleure option.

Mis à part : en passant, je pense qu'il est possible de s'éloigner complètement des "types existentiels", ce qui rend toute la situation plus claire, ce que moi ou quelqu'un d'autre suivra bientôt.

Je me retrouve à penser qu'une syntaxe autre que type aiderait aussi, précisément parce que beaucoup de gens interprètent type comme un simple alias substituable, ce qui impliquerait l'interprétation "de type potentiellement différent à chaque fois".

Je n'ai vu personne d'autre souligner que la portée est entièrement différente pour l'APIT et le RPIT.

Je pensais que le cadrage faisait toujours explicitement partie des propositions de trait d'impl, donc il n'y avait pas besoin de le « souligner ». Tout ce que vous avez dit sur la portée semble simplement réitérer ce que nous avons déjà accepté dans les RFC précédents. Je comprends que ce n'est pas évident pour tout le monde d'après la syntaxe et c'est un problème, mais ce n'est pas comme si personne ne l'avait compris auparavant. En fait, je pensais qu'une grande partie de la discussion sur la RFC 2701 portait sur ce que devrait être la portée de type Foo = impl Trait; , dans le sens de ce que l'inférence de type est et n'est pas autorisée à regarder.

Il est possible de trouver une syntaxe cohérente qui n'ait pas cette confusion.

Essayez-vous de dire que type Foo: Bar = _; est cette syntaxe, ou pensez-vous que nous ne l'avons pas encore trouvée ?

Je ne pense pas qu'il soit possible de trouver une syntaxe sans confusion similaire, non pas parce que nous sommes insuffisamment créatifs, mais parce que la plupart des programmeurs ne sont pas des théoriciens des types. Nous pouvons probablement trouver une syntaxe qui réduit la confusion à un niveau tolérable, et il existe certainement de nombreuses syntaxes qui seraient sans ambiguïté pour les vétérans de la théorie des types, mais nous n'éliminerons jamais complètement la confusion.

vous avez inventé une notation entièrement nouvelle

Je pensais que je venais de remplacer un mot-clé par un autre mot-clé. Voyez-vous un changement supplémentaire que je n'avais pas prévu ?

À bien y penser, puisque nous avons abusé de "existentiel" tout ce temps, cela signifie que existential Foo: Trait / = impl Trait ne sont probablement plus des syntaxes légitimes.

Nous avons donc besoin d'un nouveau mot-clé à mettre devant les noms qui font référence à un type de code inconnu à externe... et je fais un vide là-dessus. alias , secret , internal , etc semblent tous assez terrible, et peu susceptible d'avoir pas moins "confusion unique" que type .

À bien y penser, puisque nous avons abusé de "existentiel" tout ce temps, cela signifie que existential Foo: Trait / = impl Trait ne sont probablement plus des syntaxes légitimes.

Oui, je suis tout à fait d'accord - je pense que nous devons nous éloigner complètement du terme "existentiel"* (il y a eu quelques idées provisoires sur la façon de le faire tout en expliquant bien impl Trait ).

*(en réservant éventuellement le terme pour dyn Trait seulement)

@joshtriplett , @Ixrec : Je suis d'accord que la notation _ signifie que vous ne pouvez plus substituer dans la même mesure qu'avant, et si c'est une priorité à garder, nous aurions besoin d'une syntaxe différente.

Gardez à l'esprit que _ est déjà un cas particulier en ce qui concerne la substitution de toute façon — ce ne sont pas seulement les alias de type que cela affecte : partout où vous pouvez actuellement utiliser _ , vous empêchez une transparence référentielle totale.

Gardez à l'esprit que _ est déjà un cas particulier en ce qui concerne la substitution de toute façon — ce ne sont pas seulement les alias de type que cela affecte : partout où vous pouvez actuellement utiliser _, vous empêchez une transparence référentielle totale.

Pourriez-vous nous expliquer ce que cela signifie exactement ? Je n'étais pas au courant d'une notion de "transparence référentielle" affectée par _ .

Je suis d'accord que la notation _ signifie que vous ne pouvez plus substituer dans la même mesure qu'avant, et si c'est une priorité à garder, nous aurions besoin d'une syntaxe différente.

Je ne suis pas sûr que ce soit une _priorité_. Pour moi, c'était juste le seul argument objectif que nous ayons jamais trouvé qui semblait préférer une syntaxe à l'autre. Mais tout cela est susceptible de changer en fonction des mots-clés que nous pouvons trouver pour remplacer type .

Pourriez-vous nous expliquer ce que cela signifie exactement ? Je n'étais pas au courant d'une notion de "transparence référentielle" affectée par _ .

Ouais, désolé, je lance des mots sans les expliquer. Permettez-moi de rassembler mes pensées et je formulerai une explication plus cohérente. Cela correspond bien à une façon alternative (et potentiellement plus utile) de regarder impl Trait .

Par transparence référentielle , on entend qu'il est possible de substituer une référence à sa définition et inversement sans changement de sémantique. Dans Rust, cela ne tient clairement pas au niveau du terme pour fn . Par example:

fn foo() -> usize {
    println!("ey!");
    42
}

fn main() {
    let bar = foo();
    let baz = bar + bar;
}

si nous substituons chaque occurrence de bar à foo() (la définition de bar ), alors nous obtenons clairement une sortie différente.

Cependant, pour les alias de type, la transparence référentielle est maintenue (AFAIK) pour le moment. Si vous avez un alias :

type Foo = Definition;

Ensuite, vous pouvez faire (en évitant la capture) la substitution des occurrences de Foo pour Definition et la substitution des occurrences de Definition pour Foo sans changer la sémantique de votre programme , ou son exactitude de type.

Présentation :

type Foo = impl Bar;

signifier que chaque occurrence de Foo est du même type signifie que si vous écrivez :

fn stuff() -> Foo { .. }
fn other_stuff() -> Foo { .. }

vous ne pouvez pas remplacer les occurrences de Foo par impl Bar et vice versa. C'est-à-dire si vous écrivez :

fn stuff() -> impl Bar { .. }
fn other_stuff() -> impl Bar { .. }

les types de retour ne s'uniront pas avec Foo . Ainsi, la transparence référentielle est rompue pour les alias de type en introduisant impl Trait avec la sémantique de la RFC 2071 à l'intérieur.

Sur la transparence référentielle et type Foo = _; , à suivre... (par @varkor)

Je me retrouve à penser qu'une syntaxe autre que le type aiderait également, précisément parce que de nombreuses personnes interprètent le type comme un simple alias substituable, ce qui impliquerait l'interprétation "de type potentiellement différent à chaque fois".

Bon point. Mais le bit d'affectation = _ n'implique-t-il pas qu'il ne s'agit que d'un seul type ?

J'ai déjà écrit ça, mais...

Transparence référentielle : je pense qu'il est plus utile de considérer type comme une liaison (comme let ) au lieu d'une substitution de type préprocesseur C. Une fois que vous le regardez de cette façon, type Foo = impl Trait signifie exactement ce qu'il semble.

J'imagine que les débutants seront moins susceptibles de considérer impl Trait comme des types existentiels ou universels, mais comme "une chose qui impl sa Trait . If they want to know more, they can read the impl Trait` documentation. Une fois que vous changer la syntaxe, vous perdez la connexion entre elle et la fonctionnalité existante avec peu d'avantages. _Vous ne faites que remplacer une syntaxe potentiellement trompeuse par une autre._

Re type Foo = _ , il surcharge _ avec une signification complètement indépendante. Il peut également sembler difficile à trouver dans la documentation et/ou Google.

@lnicola Vous pouvez tout aussi bien opter const liaisons let , où la première est référentiellement transparente. Choisir let (qui n'est pas référentiellement transparent à l'intérieur de fn ) est un choix arbitraire que je ne pense pas particulièrement intuitif. Je pense que la vue intuitive des alias de type est qu'ils sont référentiellement transparents (même si ce mot n'est pas utilisé) car ce sont des alias .

Je ne considère pas non plus type comme une substitution de préprocesseur C car il doit être capturé en évitant et en respectant les génériques (pas de SFINAE). Au lieu de cela, je pense à type précisément comme je le ferais à une liaison dans un langage comme Idris ou Agda où toutes les liaisons sont pures.

J'imagine que les débutants seront moins susceptibles de considérer impl Trait comme des types existentiels par rapport à des types universels, mais comme "une chose qui implique un trait

Cela me semble être une distinction sans différence. Le jargon "existentiel" n'est pas utilisé, mais je pense que l'utilisateur le lie intuitivement au même concept que celui de type existentiel (qui n'est rien de plus que "quelque type Foo qui impls Bar" dans le contexte de Rust).

Re type Foo = _ , il surcharge _ avec une signification complètement indépendante.

Comment? type Foo = _; ici s'aligne sur l'utilisation de _ dans d'autres contextes où un type est attendu.
Cela signifie "déduire le type réel", tout comme lorsque vous écrivez .collect::<Vec<_>>() .

Il peut également sembler difficile à trouver dans la documentation et/ou Google.

Ça ne devrait pas être si difficile ? "type alias underscore" devrait, espérons-le, faire apparaître le résultat souhaité...?
Cela ne semble pas différent de la recherche de "type alias impl trait".

Google n'indexe pas les caractères spéciaux. Si ma question StackOverflow contient un trait de soulignement, Google ne l'indexera pas automatiquement pour les requêtes contenant le mot souligné

@Centril

Comment? type Foo = _; ici s'aligne sur l'utilisation de _ dans d'autres contextes où un type est attendu.
Cela signifie "déduire le type réel", tout comme lorsque vous écrivez .collect ::>().

Mais cette fonctionnalité ne déduit pas le type et ne vous donne pas un alias de type, elle crée un type existentiel qui (en dehors d'une portée limitée comme module ou crate) ne s'unifie pas avec "le type réel".

Google n'indexe pas les caractères spéciaux.

Ce n'est plus vrai (bien que peut-être dépendant des espaces blancs..?).

Mais cette fonctionnalité ne déduit pas le type et ne vous donne pas un alias de type, elle crée un type existentiel qui (en dehors d'une portée limitée comme module ou crate) ne s'unifie pas avec "le type réel".

La sémantique suggérée de type Foo = _; est une alternative à un alias de type existentiel, entièrement basé sur l'inférence. Si ce n'était pas tout à fait clair, je vais bientôt poursuivre avec quelque chose qui devrait expliquer un peu mieux les intentions.

@iopq En plus de la note de type telle sorte qu'il devienne consultable.

Vous n'obtiendrez toujours pas de bons résultats avec _ dans votre requête, pour une raison quelconque. Si vous recherchez un trait de soulignement, vous obtenez des éléments contenant le mot souligné. Si vous recherchez _ vous obtenez tout ce qui a un trait de soulignement, donc je ne sais même pas si c'est pertinent

@Centril

Choisir let (qui n'est pas référentiellement transparent à l'intérieur de fn) est un choix arbitraire que je ne pense pas particulièrement intuitif. Je pense que la vue intuitive des alias de type est qu'ils sont référentiellement transparents (même si ce mot n'est pas utilisé) car ce sont des alias.

Désolé, je ne peux toujours pas comprendre cela parce que mon intuition est complètement à l'envers.

Par exemple, si nous avons type Foo = Bar , mon intuition dit :
"Nous déclarons Foo , qui devient le même type que Bar ."

Alors, si nous écrivons type Foo = impl Bar , mon intuition dit :
"Nous déclarons Foo , qui devient un type qui implémente Bar ."

Si Foo n'est qu'un alias textuel pour impl Bar , alors ce serait super peu intuitif pour moi. J'aime penser à cela comme des alias sémantiques .

Donc, si Foo peut être remplacé par impl Bar partout où il apparaît, c'est un alias textuel , qui me rappelle le plus les macros et la métaprogrammation. Mais si Foo a reçu une signification au moment de la déclaration et peut être utilisé à plusieurs endroits avec cette signification d'origine (pas de signification contextuelle !), c'est un alias sémantique .

De plus, je ne parviens pas à comprendre la motivation derrière les types existentiels contextuels de toute façon. Seraient-ils un jour utiles, étant donné que les alias de traits peuvent réaliser exactement la même chose ?

Peut-être que je trouve la transparence référentielle peu intuitive à cause de mon expérience non-Haskell, qui sait... :) Mais en tout cas, ce n'est certainement pas le genre de comportement auquel je m'attendrais dans Rust.

@Nemo157 @stjepang

Si Foo était vraiment un alias de type pour un type existentiel

(c'est moi qui souligne). J'ai lu que « an » comme « un spécifique », ce qui signifie que f et g ne prendraient pas en charge différents types de retour concrets, car ils font référence au même type existentiel.

Il s'agit d'une utilisation abusive du terme "type existentiel", ou du moins d'une manière qui est en contradiction avec la publication de type Foo = impl Bar peut sembler faire de Foo un alias pour le type ∃ T. T: Trait - et si vous remplacez ∃ T. T: Trait partout, vous utilisez Foo , même non -textuellement , vous pouvez obtenir un type concret différent dans chaque position.

La portée de ce quantificateur ∃ T (exprimé dans votre exemple par existential type _0 ) est la chose en question. C'est serré comme ça dans APIT - l'appelant peut passer n'importe quelle valeur qui satisfait ∃ T. T: Trait . Mais ce n'est pas dans RPIT, et pas dans les déclarations existential type RFC 2071, et pas dans votre exemple de désucrage - là, le quantificateur est plus éloigné, au niveau de la fonction entière ou du module entier, et vous traitez le même T partout.

Ainsi l'ambiguïté- nous avons déjà impl Trait plaçant son quantificateur à différents endroits en fonction de sa position, alors à laquelle devons-nous nous attendre pour type T = impl Trait ? Certains sondages informels, ainsi que certaines réalisations après coup des participants au fil de discussion RFC 2071, prouvent que ce n'est pas clair dans un sens ou dans l'autre.

C'est pourquoi nous voulons nous éloigner de l'interprétation de impl Trait comme quoi que type T = _ n'a pas le même genre d'ambiguïté - il y a toujours le niveau de surface "ne peut pas copier-coller le _ à la place de T ", mais il n'y en a plus "le type unique dont T est un alias peut signifier plusieurs types concrets." (Le comportement opaque/n'unifie pas est la chose sur laquelle

transparence référentielle

Ce n'est pas parce qu'un alias de type est actuellement compatible avec la transparence référentielle que les gens s'attendent à ce

Par exemple, l'élément const est référentiel transparent (mentionné dans https://github.com/rust-lang/rust/issues/34511#issuecomment-402520768), et cela a en fait causé une confusion entre les nouveaux et les anciens utilisateurs (rust-lang-nursery/rust-clippy#1560).

Je pense donc que pour un programmeur Rust, la transparence référentielle n'est pas la première chose à laquelle ils penseraient.

@stjepang @kennytm Je ne dis pas que tout le monde s'attend à ce que les alias de type avec type Foo = impl Trait; agissent de manière référentielle transparente. Mais je pense qu'un nombre non négligeable d'utilisateurs le fera, comme en témoignent les confusions dans ce fil et ailleurs (à quoi @rpjohnst fait référence...). C'est un problème, mais peut-être pas insurmontable. C'est quelque chose à garder à l'esprit au fur et à mesure que nous avançons.

Ma réflexion actuelle sur ce qui devrait être fait dans ce domaine s'est alignée sur @varkor et @rpjohnst.

re: transparence référentielle

type Foo<T> = (T, T);

type Bar = Foo<impl Copy>;   // not equivalent to (impl Copy, impl Copy)

c'est-à-dire que même la génération de nouveaux types à chaque instance n'est pas référentiellement transparent dans le contexte des alias de types génériques.

@centril Je lève la main quand il s'agit d'attendre une transparence référentielle pour Foo dans type Foo = impl Bar; . Avec type Foo: Bar = _; cependant, je ne m'attendrais pas à une transparence référentielle.

Il est également possible que nous puissions étendre la position impl Trait retour enum impl Trait , en monomorphisant (des morceaux de) l' appelant . Cela renforce l'interprétation " impl Trait est toujours existentielle", la rapproche de dyn Trait et suggère une syntaxe abstract type qui n'utilise pas impl Trait du tout.

J'ai écrit ceci sur les internes ici : https://internals.rust-lang.org/t/extending-impl-trait-to-allow-multiple-return-types/7921

Juste une note pour quand nous stabilisons les nouveaux types existentiels - "existentiel" a toujours été destiné à être un mot-clé temporaire (selon la RFC) et (IMO) est terrible. Il faut trouver mieux avant de se stabiliser.

Le discours sur les types « existentiels » ne semble pas éclaircir les choses. Je dirais que impl Trait représente un type spécifique inféré mettant en œuvre un trait. Décrit ainsi, type Foo = impl Bar est clairement un type spécifique, toujours le même, et c'est aussi la seule interprétation qui soit réellement utile : il peut donc être utilisé dans d'autres contextes que celui à partir duquel il a été déduit, comme dans les structs.

Dans ce sens, il serait logique d'écrire également impl Trait sous la forme _ : Trait .

@rpjohnst ,

Il est également possible que nous puissions étendre la position impl Trait retour

Cela rendrait l'OMI strictement moins utile. L'intérêt des alias vers les types impl est qu'une fonction peut être définie comme renvoyant impl Foo , mais le type spécifique se propage toujours dans le programme dans d'autres structures et autres éléments. Cela fonctionnerait si le compilateur générait implicitement enum , mais pas avec la monomorphisation.

@jan-hudec Ces idées sont apparues dans les discussions sur Discord, et il y a quelques problèmes, principalement basés sur le fait que l'interprétation actuelle de return-position et argument-position impl Trait est incohérente.

Faire en sorte que impl Trait représente un type inféré spécifique est une bonne option, mais pour corriger cette incohérence, il doit s'agir d'un type d'inférence de type impl Trait . C'est probablement la façon la plus simple de procéder, mais ce n'est pas aussi simple que vous le dites.

Par exemple, une fois que impl Trait signifie « utilisez ce nouveau type d'inférence pour trouver un type polymorphe-comme-possible qui implémente Trait », type Foo = impl Bar commence à impliquer des choses sur les modules. Les règles RFC 2071 autour de la façon de déduire un montant de abstract type dire que toutes les utilisations doivent déduire indépendamment du même type, mais cette conclusion polymorphes aurait au moins laisser entendre que plus est possible. Et si jamais nous obtenions des modules paramétrés (même juste sur des durées de vie, une idée beaucoup plus plausible), il y aurait des questions autour de cette interaction.

Il y a aussi le fait que certaines personnes interpréteront toujours la syntaxe type Foo = impl Bar comme un alias pour un existentiel, qu'elles comprennent ou non le mot "existentiel" et quelle que soit la façon dont nous l'enseignons. Donc, choisir une syntaxe alternative, même si cela fonctionne avec l'interprétation basée sur l'inférence, est probablement toujours une bonne idée.

De plus, alors que la syntaxe _: Trait est en fait ce qui a inspiré la discussion autour de l'interprétation basée sur l'inférence en premier lieu, elle ne fait pas ce que nous voulons. Premièrement, l'inférence impliquée par _ n'est pas polymorphe, c'est donc une mauvaise analogie avec le reste du langage. Deuxièmement, _ implique que le type réel est visible ailleurs, tandis que impl Trait est spécifiquement conçu pour masquer le type réel.

Enfin, la raison pour laquelle j'ai écrit cette proposition de monomorphisation était de trouver un autre moyen d'unifier le sens de l'argument et de la position impl Trait retour -> impl Trait ne garantit plus un seul type concret, nous n'avons actuellement aucun moyen d'en tirer parti de toute façon. Et les solutions proposées sont ennuyeux workarounds- boilerplate supplémentaire abstract type trucs, typeof , etc tout le monde Obliger qui veut compter sur le comportement de type unique de nommer également ce type unique via abstract type syntaxe

Ces idées sont apparues dans les discussions sur Discord, et il y a quelques problèmes, principalement basés sur le fait que l'interprétation actuelle de la position de retour et de la position d'argument impl Trait est incohérente.

Personnellement, je ne trouve pas que cette incohérence soit un problème dans la pratique. La portée dans laquelle les types concrets sont déterminés pour la position d'argument par rapport à la position de retour par rapport à la position de type semble fonctionner de manière assez intuitive.

J'ai une fonction où l'appelant décide de son type de retour. Bien sûr, je ne peux pas utiliser impl Trait là-bas. Ce n'est pas aussi intuitif que vous le prétendez jusqu'à ce que vous compreniez la différence.

Personnellement, je ne trouve pas que cette incohérence soit un problème dans la pratique.

En effet. Ce que cela me suggère n'est pas que nous devrions ignorer l'incohérence, mais que nous devrions ré-expliquer la conception afin qu'elle soit cohérente (par exemple, en l'expliquant comme une inférence de type polymorphe). De cette façon, les futures extensions (RFC 2071, etc.) peuvent être comparées à la nouvelle interprétation cohérente pour éviter que les choses ne deviennent confuses.

@rpjohnst

Forcer tous ceux qui souhaitent s'appuyer sur un comportement de type unique à nommer également ce type unique via la syntaxe de type abstrait (quelle qu'elle soit) est sans doute un avantage global.

Pour certains cas, je suis d'accord avec ce sentiment, mais cela ne fonctionne pas avec les fermetures ou les générateurs, et est peu ergonomique dans de nombreux cas où vous ne vous souciez pas du type et tout ce qui vous intéresse, c'est qu'il implémente un certain trait , par exemple avec des combinateurs itérateurs.

@mikeyhew Vous inventer un nom via la syntaxe RFC 2071 abstract type . Vous devez inventer un nom, peu importe si vous allez utiliser le type unique ailleurs.

@rpjohnst oh je vois, merci d'avoir clarifié

En attendant let x: impl Trait avec impatience.

Comme un autre vote pour let x: impl Trait cela simplifiera certains des exemples de futures , voici un exemple d'exemple , actuellement il utilise une fonction juste pour avoir la possibilité d'utiliser impl Trait :

fn make_sink_async() -> impl Future<Output = Result<
    impl Sink<SinkItem = T, SinkError = E>,
    E,
>> { // ... }

à la place, cela pourrait être écrit comme une liaison let normale :

let future_sink: impl Future<Output = Result<
    impl Sink<SinkItem = T, SinkError = E>,
    E,
>> = // ...;

Je peux encadrer quelqu'un en mettant en œuvre let x: impl Trait si je le souhaite. Ce n'est pas incroyablement difficile à faire, mais certainement pas facile non plus. Un point d'entrée :

De la même manière que nous visitons le type de retour impl Trait dans https://github.com/rust-lang/rust/blob/master/src/librustc/hir/lowering.rs#L3159, nous devons visiter le type de locaux dans https ://github.com/rust-lang/rust/blob/master/src/librustc/hir/lowering.rs#L3159 et assurez-vous que leurs éléments existentiels nouvellement générés sont renvoyés avec le local.

Ensuite, lorsque vous visitez le type de locaux, assurez-vous de définir ExistentialContext sur Return pour l'activer réellement.

Cela devrait déjà nous mener très loin. Je ne sais pas si tout le chemin, ce n'est pas à 100% comme return position impl trait, mais devrait surtout se comporter comme ça.

@rpjohnst ,

Ces idées sont apparues dans les discussions sur Discord, et il y a quelques problèmes, principalement basés sur le fait que l'interprétation actuelle de return-position et argument-position impl Trait sont incohérentes.

Cela nous ramène aux champs d'application dont vous avez parlé dans votre article. Et je pense qu'ils correspondent en fait à la "parenthèse" englobante : pour la position d'argument, c'est la liste d'arguments, pour la position de retour, c'est la fonction - et pour l'alias, ce serait la portée dans laquelle l'alias est défini.

J'ai ouvert une RFC proposant une résolution de la syntaxe concrète existential type , basée sur la discussion dans ce fil, la RFC originale et les discussions synchrones : https://github.com/rust-lang/rfcs/pull /2515.

L'implémentation actuelle du type existentiel ne peut pas être utilisée pour représenter toutes les définitions de la position de retour actuelle impl Trait , car impl Trait capture chaque argument de type générique même s'il n'est pas utilisé, il devrait être possible de faire la même chose avec existential type , mais vous obtenez des avertissements de paramètres de type inutilisés : (terrain de jeu)

fn foo<T>(_: T) -> impl ::std::fmt::Display {
    5
}

existential type Bar<T>: ::std::fmt::Display;
fn bar<T>(_: T) -> Bar<T> {
    5
}

Cela peut avoir de l'importance car les paramètres de type peuvent avoir des durées de vie internes qui restreignent la durée de vie des impl Trait renvoyés même si la valeur elle-même n'est pas utilisée, supprimez les <T> de Bar dans le terrain de jeu ci-dessus pour voir que l'appel à foo échoue mais que bar fonctionne.

L'implémentation actuelle du type existentiel ne peut pas être utilisée pour représenter toutes les définitions de trait impl de position de retour actuelle

vous pouvez, c'est juste très gênant. Vous pouvez retourner un nouveau type avec un champ PhantomData + un champ de données réel et implémenter le trait en tant que transfert vers le champ de données réel

@oli-obk Merci pour les conseils supplémentaires. Avec vos conseils précédents et certains de @cramertj , je pourrais probablement

@fasihrana @Nemo157 Voir ci-dessus. Peut-être dans quelques semaines ! :-)

Quelqu'un peut-il préciser que le comportement de existential type ne capturant @Nemo157 a mentionné) est intentionnel et restera tel quel ? Je l'aime parce qu'il résout le #42940

Je l'ai mis en œuvre de cette façon très exprès

@Arnavion Oui, c'est intentionnel et correspond à la façon dont les autres déclarations d'éléments (par exemple les fonctions imbriquées) fonctionnent dans Rust.

L'interaction entre existential_type et never_type déjà été discutée ?

Peut-être que ! devrait être capable de remplir n'importe quel type existentiel, quels que soient les traits impliqués.

existential type Mystery : TraitThatIsHardToEvenStartImplementing;

fn hack_to_make_it_compile() -> Mystery { unimplemented!() }

Ou y aura-t-il un type spécial intouchable servant de niveau unimplemented!() type

@vi Je pense que cela relèverait du général "ne jamais le type ne devrait implémenter tous les traits sans aucune méthode non auto par défaut ou type associé". Je ne sais pas où cela serait suivi, cependant.

Est-il prévu d'étendre bientôt la prise en charge des types de retour de méthode de trait ?

existential type fonctionne déjà pour les méthodes de trait. Wrt impl Trait , est-ce même couvert par un RFC ?

@alexreg Je pense que cela nécessite que les GAT puissent désucrer à un type associé anonyme lorsque vous avez quelque chose comme fn foo<T>(..) -> impl Bar<T> (devient environ -> Self::AnonBar0<T> ).

@Centril vouliez -vous faire le <T> sur impl Bar là-bas ? Le comportement de capture de type implicite de impl Trait signifie que vous avez le même besoin de GAT même avec quelque chose comme fn foo<T>(self, t: T) -> impl Bar; .

@ Nemo157 non désolé je ne l'ai pas fait. Mais votre exemple illustre encore mieux le problème. Merci :)

@alexreg Je pense que cela nécessite que les GAT puissent désucrer à un type associé anonyme lorsque vous avez quelque chose comme fn foo(..) -> impl Bar(devient grosso modo -> Self::AnonBar0).

Ah, je vois. Pour être honnête, cela ne semble pas strictement nécessaire, mais c'est certainement une façon de le mettre en œuvre. Le manque de mouvement sur les GAT m'inquiète un peu cependant... je n'ai rien entendu depuis longtemps.

Triage : https://github.com/rust-lang/rust/pull/53542 a été fusionné, donc les cases à cocher pour {let,const,static} foo: impl Trait peuvent être cochées, je pense.

Pourrai-je un jour écrire :

trait Foo {
    fn GetABar() -> impl Bar;
}

??

Probablement pas. Mais il y a des plans en cours pour tout préparer afin que nous puissions obtenir

trait Foo {
    type Assoc: Bar;
    fn get_a_bar() -> Assoc;
}

impl Foo for SomeType {
    fn get_a_bar() -> impl Bar {
        SomeThingImplingBar
    }
}

Vous pouvez expérimenter cette fonctionnalité tous les soirs sous la forme de

impl Foo for SomeType {
    existential type Assoc;
    fn get_a_bar() -> Assoc {
        SomeThingImplingBar
    }
}

Un bon début pour obtenir plus d'informations à ce sujet est https://github.com/rust-lang/rfcs/pull/2071 (et tout ce qui y est lié)

@oli-obk sur rustc 1.32.0-nightly (00e03ee57 2018-11-22) , je dois également donner les limites de trait pour existential type pour travailler dans un bloc impl comme ça. C'est prévu ?

@jonhoo pouvoir spécifier les traits est utile car vous pouvez fournir plus que les traits requis

impl Foo for SomeDebuggableType {
    existential type Assoc: Bar + Debug;
    fn get_a_bar() -> Assoc {
        SomeThingImplingBarAndDebug
    }
}

fn use_debuggable_foo<F>(f: F) where F: Foo, F::Assoc: Debug {
    println!("bar is: {:?}", f.get_a_bar())
}

Les traits requis pourraient être implicitement ajoutés à un type associé existentiel, vous n'avez donc besoin que de limites lors de leur extension, mais personnellement, je préférerais la documentation locale pour devoir les mettre dans l'implémentation.

@ Nemo157 Ah, désolé, ce que je voulais dire, c'est qu'actuellement, vous devez avoir des limites là-bas. C'est-à-dire que cela ne compilera pas :

impl A for B {
    existential type Assoc;
    // ...
}

alors que cela va:

impl A for B {
    existential type Assoc: Debug;
    // ...
}

Oh, donc même dans le cas où un trait ne nécessite aucune limite du type associé, vous devez toujours donner une limite au type existentiel (qui peut être vide) ( aire de jeux ):

trait Foo {
    type Assoc;
    fn foo() -> Self::Assoc;
}

struct Bar;
impl Foo for Bar {
    existential type Assoc: ;
    fn foo() -> Self::Assoc { Bar }
}

Cela me semble être un cas limite, avoir un type existentiel sans limite signifie qu'il ne fournit aucune opération aux utilisateurs (autres que les traits automatiques), alors à quoi cela pourrait-il être utilisé ?

A noter également qu'il n'y a aucun moyen de faire la même chose avec -> impl Trait , -> impl () est une erreur de syntaxe et -> impl à lui seul donne error: at least one trait must be specified ; si la syntaxe du type existentiel devient type Assoc = impl Debug; ou similaire, il semble qu'il n'y aurait aucun moyen de spécifier le type associé sans au moins un trait lié.

@ Nemo157 oui, je ne l'ai réalisé que parce que j'ai essayé littéralement le code que vous avez suggéré ci-dessus, et cela n'a pas fonctionné :p J'ai en quelque sorte supposé que cela déduirait les limites du trait. Par example:

trait Foo {
    type Assoc: Future<Output = u32>;
}

struct Bar;
impl Foo for Bar {
    existential type Assoc;
}

Il semblait raisonnable de ne pas avoir à spécifier Future<Output = u32> une deuxième fois, mais cela ne fonctionne pas. Je suppose que existential type Assoc: ; (qui semble également être une syntaxe super étrange) ne fera pas cette inférence non plus?

trait Foo {
    type Assoc;
    fn foo() -> Self::Assoc;
}

struct Bar;
impl Foo for Bar {
    existential type Assoc: ;
    fn foo() -> Self::Assoc { Bar }
}

Cela me semble être un cas limite, avoir un type existentiel sans limite signifie qu'il ne fournit aucune opération aux utilisateurs (autres que les traits automatiques), alors à quoi cela pourrait-il être utilisé ?

Ceux-ci ne pourraient-ils pas être utilisés pour la consommation dans la même implémentation de trait ? Quelque chose comme ça:

trait Foo {
    type Assoc;
    fn create_constructor() -> Self::Assoc;
    fn consume(marker: Self::Assoc) -> Self;
    fn consume_box(marker: Self::Assoc) -> Box<Foo>;
}

C'est un peu artificiel, mais cela pourrait être utile - je pourrais imaginer une situation où une partie préliminaire doit être construite avant la vraie structure pour des raisons de durée de vie. Ou ça peut être quelque chose comme :

trait MarkupSystem {
    type Cache;
    fn create_cache() -> Cache;
    fn translate(cache: &mut Self::Cache, input: &str) -> String;
}

Dans ces deux cas, existential type Assoc; serait utile.

Quelle est la bonne manière de définir les types associés pour impl Trait ?

Par exemple, si j'ai un trait Action et que je veux m'assurer que l'implémentation du type associé au trait est envoyable, puis-je faire quelque chose comme ceci :

pub trait Action {
    type Result;
    fn call(&self) -> Self::Result;
}

impl MyStruct {
    pub fn new(name: String) -> impl Action 
    where 
        Return::Result: Send //This Return should be the `impl Action`
    {
        ActionImplementation::new()
    }
}

Est-ce que quelque chose n'est actuellement pas possible?

@acycliczebra Je pense que la syntaxe pour cela est -> impl Action<Result = impl Send> - c'est la même syntaxe que, par exemple, -> impl Iterator<Item = u32> utilisant simplement un autre type anonyme impl Trait .

Y a-t-il eu des discussions sur l'extension impl Trait syntaxe

struct Iter<'a> {
    inner: std::collections::hash_map::Iter<'a, i32, i32>,
}

Ce serait utile dans les situations où je ne me soucie pas vraiment du type réel tant qu'il satisfait certaines limites de traits. Cet exemple est simple, mais j'ai rencontré des situations dans le passé où j'écris des types très longs avec un tas de paramètres de type imbriqués, et c'est vraiment inutile parce que je ne me soucie vraiment de rien sauf que c'est un ExactSizeIterator .

Cependant IIRC, je ne pense pas qu'il existe un moyen de spécifier plusieurs limites avec impl Trait pour le moment, donc je perdrais des choses utiles comme Clone .

@AGausmann La dernière discussion sur le sujet se trouve sur https://github.com/rust-lang/rfcs/pull/2515. Cela vous permettrait de dire type Foo = impl Bar; struct Baz { field: Foo } ... . Je pense que nous pouvons considérer field: impl Trait comme du sucre après avoir stabilisé type Foo = impl Bar; . Cela ressemble à une extension de commodité raisonnable et conviviale pour les macros.

@Centril ,

Je pense que nous pourrions vouloir considérer field: impl Trait comme du sucre

Je ne pense pas que ce serait raisonnable. Un champ struct doit toujours avoir un type concret, vous devez donc indiquer au compilateur le retour de la fonction à laquelle il est lié. Cela pourrait le déduire, mais si vous avez plusieurs fonctions, il ne serait pas si facile de trouver laquelle il s'agit - et la politique habituelle de Rust est d'être explicite dans de tels cas.

Cela pourrait le déduire, mais si vous avez plusieurs fonctions, il ne serait pas si facile de trouver laquelle il s'agit

Vous augmenteriez l'exigence de définition des utilisations du type parent. Ce seraient alors toutes ces fonctions dans le même module qui renverraient le type parent. Cela ne me semble pas si difficile à trouver. Je pense cependant que nous voulons régler l'histoire sur type Foo = impl Bar; avant d'aller de l'avant avec les extensions.

Je pense avoir trouvé un bogue dans l'implémentation actuelle de existential type .


Code

trait Collection {
    type Element;
}
impl<T> Collection for Vec<T> {
    type Element = T;
}

existential type Existential<T>: Collection<Element = T>;

fn return_existential<I>(iter: I) -> Existential<I::Item>
where
    I: IntoIterator,
    I::Item: Collection,
{
    let item = iter.into_iter().next().unwrap();
    vec![item]
}


Erreur

error: type parameter `I` is part of concrete type but not used in parameter list for existential type
  --> src/lib.rs:16:1
   |
16 | / {
17 | |     let item = iter.into_iter().next().unwrap();
18 | |     vec![item]
19 | | }
   | |_^

error: defining existential type use does not fully define existential type
  --> src/lib.rs:12:1
   |
12 | / fn return_existential<I>(iter: I) -> Existential<I::Item>
13 | | where
14 | |     I: IntoIterator,
15 | |     I::Item: Collection,
...  |
18 | |     vec![item]
19 | | }
   | |_^

error: could not find defining uses
  --> src/lib.rs:10:1
   |
10 | existential type Existential<T>: Collection<Element = T>;
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Cour de récréation

Vous pouvez également le trouver sur stackoverflow .

Je ne suis pas sûr à 100% que nous puissions prendre en charge ce cas, mais ce que vous pouvez faire est de réécrire la fonction pour avoir deux paramètres génériques :

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=b4e53972e35af8fb40ffa9a735c6f6b1

fn return_existential<I, J>(iter: I) -> Existential<J>
where
    I: IntoIterator<Item = J>,
{
    let item = iter.into_iter().next().unwrap();
    vec![item]
}

Merci!
Oui, c'est ce que j'ai fait comme posté sur le post stackoverflow:

fn return_existential<I, T>(iter: I) -> Existential<T>
where
    I: IntoIterator<Item = T>,
    I::Item: Collection,
{
    let item = iter.into_iter().next().unwrap();
    vec![item]
}

Existe-t-il des plans pour que impl Trait soit disponible dans un contexte de trait ?
Non seulement comme type associé, mais aussi comme valeur de retour dans les méthodes.

impl trait in traits est une fonction distincte de celles qui sont suivies ici et n'a pas actuellement de RFC. Il y a une assez longue histoire de conceptions dans cet espace, et la poursuite de l'itération est suspendue jusqu'à ce que la mise en œuvre de 2071 (type existentiel) soit stabilisée, ce qui est bloqué sur les problèmes de mise en œuvre ainsi que sur la syntaxe non résolue (qui a une RFC distincte).

@cramertj La syntaxe est presque résolue. Je crois que le principal bloqueur est GAT maintenant.

@alexreg : https://github.com/rust-lang/rfcs/pull/2515 attend toujours sur @withoutboats.

@varkor Oui, je suis juste optimiste, ils verront bientôt le jour avec cette RFC. ;-)

Est-ce que quelque chose comme ce qui suit sera possible?

#![feature(existential_type)]

trait MyTrait {}

existential type Interface: MyTrait;

struct MyStruct {}
impl MyTrait for MyStruct {}

fn with<F, U>(cb: F) -> U
where
    F: FnOnce(&mut Interface) -> U
{
    let mut s = MyStruct {};
    cb(&mut s)
}

Vous pouvez le faire maintenant, mais uniquement avec une fonction hint pour spécifier le type concret de Interface

#![feature(existential_type)]

trait MyTrait {}

existential type Interface: MyTrait;

struct MyStruct {}
impl MyTrait for MyStruct {}

fn with<F, U>(cb: F) -> U
where
    F: FnOnce(&mut Interface) -> U
{

    fn hint(x: &mut MyStruct) -> &mut Interface { x }

    let mut s = MyStruct {};
    cb(hint(&mut s))
}

Comment l'écririez-vous si le rappel pouvait choisir son type d'argument ? En fait, nvm, je suppose que vous pouvez résoudre ce problème via un générique normal.

@CryZe Ce que vous recherchez n'est pas lié à impl Trait . Voir https://github.com/rust-lang/rfcs/issues/2413 pour tout ce que je sais à ce sujet.

Cela ressemblerait potentiellement à quelque chose comme ça :

trait MyTrait {}

struct MyStruct {}
impl MyTrait for MyStruct {}

fn with<F, U>(cb: F) -> U
where
    F: for<I: Interface> FnOnce(&mut I) -> U
{
    let mut s = MyStruct {};
    cb(hint(&mut s))
}

@KrishnaSannasi Ah, intéressant. Merci!

Est-ce censé fonctionner ?

#![feature(existential_type)]

trait MyTrait {
    type AssocType: Send;
    fn ret(&self) -> Self::AssocType;
}

impl MyTrait for () {
    existential type AssocType: Send;
    fn ret(&self) -> Self::AssocType {
        ()
    }
}

impl<'a> MyTrait for &'a () {
    existential type AssocType: Send;
    fn ret(&self) -> Self::AssocType {
        ()
    }
}

trait MyLifetimeTrait<'a> {
    type AssocType: Send + 'a;
    fn ret(&self) -> Self::AssocType;
}

impl<'a> MyLifetimeTrait<'a> for &'a () {
    existential type AssocType: Send + 'a;
    fn ret(&self) -> Self::AssocType {
        *self
    }
}

Devons-nous conserver le mot-clé existential dans la langue pour la fonctionnalité existential_type ?

@jethrogb Oui. Le fait que ce ne soit pas le cas actuellement est un bug.

@cramertj D'accord. Dois-je déposer un problème séparé pour cela ou mon message ici est-il suffisant ?

Déposer un problème serait génial, merci! :)

Devons-nous conserver le mot-clé existential dans la langue pour la fonctionnalité existential_type ?

Je pense que l'intention est de déprécier immédiatement cela lorsque la fonctionnalité type-alias-impl-trait est implémentée (c'est-à-dire mise dans un lint) et éventuellement de la supprimer de la syntaxe.

Quelqu'un peut peut-être clarifier cependant.

Clôturant ceci en faveur d'un méta-problème qui suit impl Trait plus généralement : https://github.com/rust-lang/rust/issues/63066

pas un seul bon exemple nulle part sur la façon d'utiliser impl Trait, très triste

Cette page vous a été utile?
0 / 5 - 0 notes