Rust: Problème de suivi pour la spécialisation (RFC 1210)

Créé le 23 févr. 2016  ·  236Commentaires  ·  Source: rust-lang/rust

Il s'agit d'un problème de suivi pour la spécialisation (rust-lang / rfcs # 1210).

Principales étapes de mise en œuvre:

  • [x] Terrain https://github.com/rust-lang/rust/pull/30652 =)
  • [] Restrictions concernant la livraison à vie (actuellement un trou de solidité )
  • [] default impl (https://github.com/rust-lang/rust/issues/37653)
  • [] Intégration avec les consts associés
  • [] Les limites ne sont pas toujours correctement appliquées (https://github.com/rust-lang/rust/issues/33017)
  • [] Devrions-nous autoriser les impls vides si le parent n'a pas de membres default ? https://github.com/rust-lang/rust/issues/48444
  • [] implémenter les impls "toujours applicables" https://github.com/rust-lang/rust/issues/48538
  • [] décrire et tester les conditions de cycle précises autour de la création du graphe de spécialisation (voir par exemple ce commentaire , qui a noté que nous avons une logique très prudente ici aujourd'hui)

Questions non résolues de la RFC:

  • Le type associé devrait-il être spécialisable?
  • Quand la projection doit-elle révéler un default type ? Jamais pendant le typeck? Ou quand monomorphe?
  • Les éléments de trait par défaut doivent-ils être considérés default (c'est-à-dire spécialisables)?
  • Devrions-nous avoir default impl (où tous les éléments sont default ) ou partial impl (où default est opt-in); voir https://github.com/rust-lang/rust/issues/37653#issuecomment -616116577 pour quelques exemples pertinents où default impl est limitatif.
  • Comment gérer la dispatchabilité à vie?

Notez que la fonctionnalité specialization telle qu'implémentée actuellement est défectueuse , ce qui signifie qu'elle peut provoquer un comportement indéfini sans code unsafe . min_specialization évite la plupart des pièges .

A-specialization A-traits B-RFC-approved B-RFC-implemented B-unstable C-tracking-issue F-specialization T-lang

Commentaire le plus utile

J'ai utilisé #[min_specialization] dans une bibliothèque expérimentale que je développais donc j'ai pensé partager mes expériences. Le but est d'utiliser la spécialisation dans sa forme la plus simple: avoir des cas étroits avec des implémentations plus rapides que le cas général. En particulier, pour avoir des algorithmes cryptographiques dans le cas général exécutés en temps constant, mais si toutes les entrées sont marquées Public pour avoir une version spécialisée qui s'exécute dans un temps variable plus rapide (car si elles sont publiques, nous ne se soucier des fuites d'informations à leur sujet via le temps d'exécution). De plus, certains algorithmes sont plus rapides selon que le point de la courbe elliptique est normalisé ou non. Pour que cela fonctionne, nous commençons par

#![feature(rustc_attrs, min_specialization)]

Ensuite, si vous devez créer un trait _specialization predicate_ comme expliqué dans la spécialisation minimale maximale, vous marquez la déclaration de trait avec #[rustc_specialization_trait] .

Toute ma spécialisation se fait dans ce fichier et voici un exemple de trait de prédicat de spécialisation.

La fonctionnalité fonctionne et fait exactement ce dont j'ai besoin. Ceci utilise évidemment un marqueur interne rustc et est donc susceptible de se casser sans avertissement.

Le seul commentaire négatif est que je ne pense pas que le mot-clé default sens. Essentiellement, ce que signifie default ce moment est: "cet impl est spécialisable donc interprétez les impls qui couvrent un sous-ensemble de celui-ci comme une spécialisation de celui-ci plutôt qu'un impl en conflit". Le problème est que cela conduit à un code très étrange:

https://github.com/LLFourn/secp256kfun/blob/6766b60c02c99ca24f816801fe876fed79643c3a/secp256kfun/src/op.rs#L196 -L206

Ici le second impl est spécialisé dans le premier mais c'est aussi default . La signification de default semble être perdue. Si vous regardez le reste des impls, il est assez difficile de savoir quels impls sont spécialisés. De plus, quand je faisais un impl erroné qui chevauchait un impl existant, il était souvent difficile de comprendre où je me suis trompé.

Il me semble que ce serait plus simple si tout était spécialisable et lorsque vous spécialisez quelque chose, vous déclarez précisément ce que vous êtes en train de spécialiser. Transformer l'exemple de la RFC en ce que j'avais en tête:

impl<A, T> Extend<A, T> for Vec<A> where T: IntoIterator<Item=A>
{
    // no need for default
    fn extend(&mut self, iterable: T) {
        ...
    }
}

// We declare explicitly which impl we are specializing repeating all type bounds etc
specialize impl<A, T> Extend<A, T> for Vec<A> where T: IntoIterator<Item=A>
    // And then we declare explicitly how we are making this impl narrower with ‘when’.
    // i.e. This impl is like the first except replace all occurances of ‘T’ with ‘&'a [A]’
    when<'a> T = &'a [A]
{
    fn extend(&mut self, iterable: &'a [A]) {
        ...
    }
}

Tous les 236 commentaires

Quelques questions ouvertes supplémentaires:

  • Doit-on revoir les règles orphelines à la lumière de la spécialisation? Existe-t-il des moyens de rendre les choses plus flexibles maintenant?
  • Devrions-nous étendre la «règle de chaîne» dans la RFC à quelque chose de plus expressif, comme la soi-disant «règle de treillis»?
  • En lien avec les deux éléments ci-dessus, comment le raisonnement négatif s'intègre-t-il dans l'histoire? Pouvons-nous récupérer le raisonnement négatif dont nous avons besoin par une utilisation suffisamment intelligente des règles de spécialisation / orphelines, ou devrions-nous le rendre plus de première classe?

Je ne suis pas sûr que la spécialisation change les règles orphelines:

  • Les règles orphelines de «liaison» doivent rester les mêmes, sinon vous n'auriez pas de liaison sécurisée.
  • Je ne pense pas que les règles orphelines de «compatibilité future» devraient changer. Ajouter un impl non-spécialisable sous vous serait toujours un changement radical.

Pire que cela, les règles orphelines de «compatibilité future» maintiennent la spécialisation cross-crate sous un contrôle assez lourd. Sans eux, les impls par défaut laissant leurs méthodes ouvertes s'aggravent.

Je n'ai jamais aimé le raisonnement négatif explicite. Je pense que la spécialisation totale du raisonnement négatif est un bon compromis.

Cela devrait-il être autorisé avec la spécialisation telle qu'implémentée? Ou est-ce que je manque quelque chose?
http://is.gd/3Ul0pe

Même chose avec celui-ci, on aurait pu s'attendre à ce qu'il compile: http://is.gd/RyFIEl

Il semble y avoir des bizarreries dans la détermination du chevauchement lorsque des types associés sont impliqués. Ceci compile: http://is.gd/JBPzIX , alors que ce code effectivement identique ne le fait pas: http://is.gd/0ksLPX

Voici un morceau de code que je m'attendais à compiler avec spécialisation:

http://is.gd/3BNbfK

#![feature(specialization)]

use std::str::FromStr;

struct Error;

trait Simple<'a> {
    fn do_something(s: &'a str) -> Result<Self, Error>;
}

impl<'a> Simple<'a> for &'a str {
     fn do_something(s: &'a str) -> Result<Self, Error> {
        Ok(s)
    }
}

impl<'a, T: FromStr> Simple<'a> for T {
    fn do_something(s: &'a str) -> Result<Self, Error> {
        T::from_str(s).map_err(|_| Error)
    }
}

fn main() {
    // Do nothing. Just type check.
}

La compilation échoue avec le compilateur citant des conflits d'implémentation. Notez que &str n'implémente pas FromStr , il ne devrait donc pas y avoir de conflit.

@sgrif

J'ai eu le temps de regarder les deux premiers exemples. Voici mes notes.

Exemple 1

Premier cas, vous avez:

  • FromSqlRow<ST, DB> for T where T: FromSql<ST, DB>
  • FromSqlRow<(ST, SU), DB> for (T, U) where T: FromSqlRow<ST, DB>, U: FromSqlRow<SU, DB>,

Le problème est que ces impls se chevauchent mais aucun n'est plus spécifique que l'autre:

  • Vous pouvez potentiellement avoir un T: FromSql<ST, DB>T n'est pas une paire (donc il correspond au premier impl mais pas au second).
  • Vous pouvez potentiellement avoir un (T, U) où:

    • T: FromSqlRow<ST, DB> ,

    • U: FromSqlRow<SU, DB> , mais _pas_

    • (T, U): FromSql<(ST, SU), DB>

    • (donc le deuxième impl correspond, mais pas le premier)

  • Les deux impls se chevauchent car vous pouvez avoir un (T, U) tel que:

    • T: FromSqlRow<ST, DB>

    • U: FromSqlRow<SU, DB>

    • (T, U): FromSql<(ST, SU), DB>

C'est le genre de situation que lattice impls permettrait - vous devriez écrire un troisième impl pour le cas de chevauchement et dire ce qu'il devrait faire. Alternativement, les impls de traits négatifs peuvent vous donner un moyen d'éliminer les chevauchements ou de modifier les correspondances possibles.

Exemple 2

Vous avez:

  • Queryable<ST, DB> for T where T: FromSqlRow<ST, DB>
  • Queryable<Nullable<ST>, DB> for Option<T> where T: Queryable<ST, DB>

Celles-ci se chevauchent car vous pouvez avoir Option<T> où:

  • T: Queryable<ST, DB>
  • Option<T>: FromSqlRow<Nullable<ST>, DB>

Mais aucun des deux impl n'est plus spécifique:

  • Vous pouvez avoir un T tel que T: FromSqlRow<ST, DB> mais T n'est pas un Option<U> (correspond au premier impl mais pas au second)
  • Vous pouvez avoir un Option<T> tel que T: Queryable<ST, DB> mais pas Option<T>: FromSqlRow<Nullable<ST>, DB>

@SergioBenitez

La compilation échoue avec le compilateur citant des conflits d'implémentation. Notez que &str n'implémente pas FromStr , il ne devrait donc pas y avoir de conflit.

Le problème est que le compilateur suppose prudemment que &str pourrait venir implémenter FromStr dans le futur. Cela peut sembler idiot pour cet exemple, mais en général, nous ajoutons de nouveaux impls tout le temps, et nous voulons protéger le code en aval de la rupture lorsque nous ajoutons ces impls.

C'est un choix conservateur, et c'est quelque chose que nous voudrions peut-être nous détendre avec le temps. Vous pouvez obtenir le contexte ici:

Merci d'avoir clarifié ces deux cas. C'est tout à fait logique maintenant

Le mar 22 mars 2016, 18 h 34, Aaron Turon [email protected] a écrit:

@SergioBenitez https://github.com/SergioBenitez

La compilation échoue avec le compilateur citant des conflits d'implémentation. Remarque
que & str n'implémente pas FromStr, il ne devrait donc pas y avoir de conflit.

Le problème est que le compilateur suppose de manière prudente que & str
pourrait venir implémenter FromStr dans le futur. Cela peut sembler idiot pour
cet exemple, mais en général, nous ajoutons de nouveaux impls tout le temps, et nous voulons
protéger le code en aval de la rupture lorsque nous ajoutons ces impls.

C'est un choix conservateur, et c'est quelque chose que nous voudrions peut-être nous détendre
heures supplémentaires. Vous pouvez obtenir le contexte ici:

-
http://smallcultfollowing.com/babysteps/blog/2015/01/14/little-orphan-impls/

-
Vous recevez cela parce que vous avez été mentionné.
Répondez directement à cet e-mail ou affichez-le sur GitHub
https://github.com/rust-lang/rust/issues/31844#issuecomment -200093757

@aturon

Le problème est que le compilateur suppose prudemment que & str pourrait venir implémenter FromStr dans le futur. Cela peut sembler idiot pour cet exemple, mais en général, nous ajoutons de nouveaux impls tout le temps, et nous voulons protéger le code en aval de la rupture lorsque nous ajoutons ces impls.

N'est-ce pas exactement ce à quoi la spécialisation tente de répondre? Avec la spécialisation, je m'attendrais à ce que même si une implémentation de FromStr pour &str était ajoutée à l'avenir, l'implémentation directe du trait Simple pour &str aurait préséance.

@SergioBenitez vous devez mettre default fn dans l'implément plus général. Votre
l'exemple n'est pas spécialisable.

Le mar 22 mars 2016, 18:54 Sergio Benitez [email protected]
a écrit:

@aturon https://github.com/aturon

Le problème est que le compilateur suppose de manière prudente que & str
pourrait venir implémenter FromStr dans le futur. Cela peut sembler idiot pour ça
exemple, mais en général, nous ajoutons de nouveaux impls tout le temps, et nous voulons
protéger le code en aval de la rupture lorsque nous ajoutons ces impls.

N'est-ce pas exactement ce à quoi la spécialisation tente de répondre? Avec
spécialisation, je m'attendrais à ce que même si une implémentation de FromStr
pour & str ont été ajoutés à l'avenir, l'implémentation directe pour le
trait pour & str aurait la priorité.

-
Vous recevez cela parce que vous avez été mentionné.
Répondez directement à cet e-mail ou affichez-le sur GitHub
https://github.com/rust-lang/rust/issues/31844#issuecomment -200097995

Je pense que les éléments de trait "par défaut" étant automatiquement considérés comme default semblent déroutants. Vous voudrez peut-être à la fois la paramétrie pour un trait comme dans Haskell, etc. en plus d'assouplir les impl s. Vous ne pouvez pas non plus facilement grep pour eux comme vous le pouvez pour default . Il n'est pas difficile à la fois de taper le mot-clé default et de donner une implémentation par défaut, mais ils ne peuvent pas être séparés tels quels. De plus, si l'on veut clarifier le langage, alors ces éléments de trait «par défaut» pourraient être renommés en éléments de «trait proposé» dans la documentation.

Note de # 32999 (commentaire) : si nous

@Stebalien

Pourquoi ça ne marche pas? L'astuce limite la spécialisation à un trait privé. Vous ne pouvez pas spécialiser le trait privé si vous ne pouvez pas y accéder.

@ arielb1 Ah. Bon point. Dans mon cas, le trait n'est pas privé.

Je ne pense pas que le raisonnement "externe ne puisse pas se spécialiser parce que la compatibilité ascendante orpheline + règle de cohérence" est particulièrement intéressant ou utile. Surtout lorsque nous ne nous engageons pas sur nos règles de cohérence spécifiques.

Existe-t-il un moyen d'accéder à un default impl remplacé? Si c'est le cas, cela pourrait aider à construire des tests. Voir Design By Contract et libhoare .

Autoriser la projection des types associés par défaut pendant la vérification de type permettra d'appliquer l'inégalité de type au moment de la compilation: https://gist.github.com/7c081574958d22f89d434a97b626b1e4

#![feature(specialization)]

pub trait NotSame {}

pub struct True;
pub struct False;

pub trait Sameness {
    type Same;
}

mod internal {
    pub trait PrivSameness {
        type Same;
    }
}

use internal::PrivSameness;

impl<A, B> Sameness for (A, B) {
    type Same = <Self as PrivSameness>::Same;
}

impl<A, B> PrivSameness for (A, B) {
    default type Same = False;
}
impl<A> PrivSameness for (A, A) {
    type Same = True;
}

impl<A, B> NotSame for (A, B) where (A, B): Sameness<Same=False> {}

fn not_same<A, B>() where (A, B): NotSame {}

fn main() {
    // would compile
    not_same::<i32, f32>();

    // would not compile
    // not_same::<i32, i32>();
}

modifié par le commentaire de @burdges

Juste pour fyi @rphmeier, il faut probablement éviter is.gd car il ne résout pas pour les utilisateurs de Tor en raison de l'utilisation de CloudFlare. GitHub fonctionne très bien avec les URL complètes. Et play.rust-lang.org fonctionne bien sur Tor.

@burdges FWIW play.rust-lang.org utilise lui-même is.gd pour son bouton "Raccourcir".

Cela peut probablement être changé, cependant: https://github.com/rust-lang/rust-playpen/blob/9777ef59b/static/web.js#L333

utiliser comme ça (https://is.gd/Ux6FNs):

#![feature(specialization)]
pub trait Foo {}
pub trait Bar: Foo {}
pub trait Baz: Foo {}

pub trait Trait {
    type Item;
}

struct Staff<T> { }

impl<T: Foo> Trait for Staff<T> {
    default type Item = i32;
}

impl<T: Foo + Bar> Trait for Staff<T> {
    type Item = i64;
}

impl<T: Foo + Baz> Trait for Staff<T> {
    type Item = f64;
}

fn main() {
    let _ = Staff { };
}

Erreur :

error: conflicting implementations of trait `Trait` for type `Staff<_>`: [--explain E0119]
  --> <anon>:20:1
20 |> impl<T: Foo + Baz> Trait for Staff<T> {
   |> ^
note: conflicting implementation is here:
  --> <anon>:16:1
16 |> impl<T: Foo + Bar> Trait for Staff<T> {
   |> ^

error: aborting due to previous error

Est-ce que feture specialization charge cela, et existe-t-il actuellement d'autres types d'implémentations?

@zitsen

Ces impls ne sont pas autorisés par la conception de spécialisation actuelle, car ni T: Foo + Bar ni T: Foo + Baz sont plus spécialisés que les autres. Autrement dit, si vous avez des T: Foo + Bar + Baz , il n'est pas clair quel impl devrait "gagner".

Nous avons quelques réflexions sur un système plus expressif qui vous permettrait de _aussi_ donner un impl pour T: Foo + Bar + Baz et donc de lever l'ambiguïté, mais cela n'a pas encore été entièrement proposé.

Si le trait négatif limite trait Baz: !Bar ever land, cela pourrait également être utilisé avec la spécialisation pour prouver que les ensembles de types qui implémentent Bar et ceux qui implémentent Baz sont distincts et individuellement spécialisables.

Il semble que la réponse de T: Foo + Bar + Baz aiderait également.

Ignorez simplement cela, j'ai encore quelque chose à voir avec mon cas, et toujours excitant pour le specialization et d'autres fonctionnalités.

Merci @aturon @rphmeier .

J'ai joué avec la spécialisation ces derniers temps, et je suis tombé sur ce cas étrange:

#![feature(specialization)]

trait Marker {
    type Mark;
}

trait Foo { fn foo(&self); }

struct Fizz;

impl Marker for Fizz {
    type Mark = ();
}

impl Foo for Fizz {
    fn foo(&self) { println!("Fizz!"); }
}

impl<T> Foo for T
    where T: Marker, T::Mark: Foo
{
    default fn foo(&self) { println!("Has Foo marker!"); }
}

struct Buzz;

impl Marker for Buzz {
    type Mark = Fizz;
}

fn main() {
    Fizz.foo();
    Buzz.foo();
}

Sortie du compilateur:

error: conflicting implementations of trait `Foo` for type `Fizz`: [--explain E0119]
  --> <anon>:19:1
19 |> impl<T> Foo for T
   |> ^
note: conflicting implementation is here:
  --> <anon>:15:1
15 |> impl Foo for Fizz {
   |> ^

parc à bébé

Je pense que ce qui précède _devrait_ se compiler, et il y a deux variantes intéressantes qui fonctionnent comme prévu:

1) Suppression de la liaison where T::Mark: Fizz :

impl<T> Foo for T
    where T: Marker //, T::Mark: Fizz
{
    // ...
}

parc à bébé

2) Ajout d'un "alias lié au trait":

trait FooMarker { }
impl<T> FooMarker for T where T: Marker, T::Mark: Foo { }

impl<T> Foo for T where T: FooMarker {
    // ...
}

parc à bébé

(Ce qui ne fonctionne pas si Marker est défini dans une caisse séparée (!), Voir cet exemple de dépôt )

Je pense également que ce problème pourrait être lié à # 20400 d'une manière ou d'une autre

EDIT : J'ai ouvert un problème à ce sujet: # 36587

Je rencontre un problème de spécialisation. Je ne sais pas s'il s'agit d'un problème d'implémentation ou d'un problème dans la manière dont la spécialisation est spécifiée.

use std::vec::IntoIter as VecIntoIter;

pub trait ClonableIterator: Iterator {
    type ClonableIter;

    fn clonable(self) -> Self::ClonableIter;
}

impl<T> ClonableIterator for T where T: Iterator {
    default type ClonableIter = VecIntoIter<T::Item>;

    default fn clonable(self) -> VecIntoIter<T::Item> {
        self.collect::<Vec<_>>().into_iter()
    }
}

impl<T> ClonableIterator for T where T: Iterator + Clone {
    type ClonableIter = T;

    #[inline]
    fn clonable(self) -> T {
        self
    }
}

( parc )
(au fait, ce serait bien si ce code finissait par atterrir dans le stdlib un jour)

Ce code échoue avec:

error: method `clonable` has an incompatible type for trait:
 expected associated type,
    found struct `std::vec::IntoIter` [--explain E0053]
  --> <anon>:14:5
   |>
14 |>     default fn clonable(self) -> VecIntoIter<T::Item> {
   |>     ^

Changer la valeur de retour en Self::ClonableIter donne l'erreur suivante:

error: mismatched types [--explain E0308]
  --> <anon>:15:9
   |>
15 |>         self.collect::<Vec<_>>().into_iter()
   |>         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected associated type, found struct `std::vec::IntoIter`
note: expected type `<T as ClonableIterator>::ClonableIter`
note:    found type `std::vec::IntoIter<<T as std::iter::Iterator>::Item>`

Apparemment, vous ne pouvez pas faire référence au type concret d'un type associé par défaut, que je trouve assez limitatif.

@tomaka cela devrait fonctionner, le texte RFC a ceci:

impl<T> Example for T {
    default type Output = Box<T>;
    default fn generate(self) -> Box<T> { Box::new(self) }
}

impl Example for bool {
    type Output = bool;
    fn generate(self) -> bool { self }
}

(https://github.com/rust-lang/rfcs/blob/master/text/1210-impl-specialization.md#the-default-keyword)

Ce qui semble assez similaire à votre cas pour être pertinent.

@aatch cet exemple ne semble pas compiler avec la définition intuitive du trait d'exemple: https://play.rust-lang.org/?gist=97ff3c2f7f3e50bd3aef000dbfa2ca4e&version=nightly&backtrace=0

le code de spécialisation interdit explicitement cela - voir # 33481, que je pensais initialement être une erreur mais qui s'est avéré être un problème de diagnostic. Mes PR pour améliorer les diagnostics ici sont passés inaperçus, et je ne les ai pas entretenus avec le dernier maître depuis un certain temps.

@rphmeier, le texte RFC suggère qu'il devrait être autorisé, cet exemple en est copié.

J'ai joué avec du code qui pourrait bénéficier d'une spécialisation. Je pense fermement que nous devrions opter pour la règle du treillis plutôt que du chaînage - cela semble naturel et c'était le seul moyen d'obtenir la flexibilité dont j'avais besoin (afaict).

Si nous options pour default sur impl ainsi que sur des éléments individuels, pourrions-nous imposer que si un élément est remplacé, alors ils doivent tous l'être? Cela nous permettrait de raisonner en fonction du type précis d'un type d'assoc par défaut (par exemple) dans les autres éléments, ce qui semble être une augmentation utile de l'expressivité.

Les éléments suivants devraient-ils être autorisés? Je veux spécialiser un type pour qu'ArrayVec soit Copy lorsque son type d'élément est Copy, et qu'il ait sinon un destructeur. J'essaye de l'accomplir en utilisant un champ interne qui est remplacé par la spécialisation.

J'espérais que cela compilerait, c'est-à-dire qu'il déduit la possibilité de copie des champs de ArrayVec<A> des types de champs sélectionnés par le A: Copy + Array lié (extrait compilable sur le terrain de jeu) .

impl<A: Copy + Array> Copy for ArrayVec<A>
    //where <A as Repr>::Data: Copy
{ }

La clause where commentée n'est pas souhaitée car elle expose un type privé Repr dans l'interface publique. (C'est aussi des ICE de toute façon).

Edit: J'avais déjà oublié que j'avais signalé le problème # 33162 à ce sujet, je suis désolé.

Suivi de mon commentaire, mon cas d'utilisation réel:

// Ideal version

trait Scannable {}

impl<T: FromStr> Scannable for T {}
impl<T: FromStr> Scannable for Result<T, ()> {}

// But this doesn't follow from the specialisation rules because Result: !FromStr
// Lattice rule would allow filling in that gap or negative reasoning would allow specifying it.

// Second attempt

trait FromResult {
    type Ok;
    fn from(r: Result<Self::Ok, ()>) -> Self;
}

impl<T> Scannable for T {
    default type Ok = T;
    default fn from(r: Result<T, ()>) -> Self {...} // error can't assume Ok == T, could do this if we had `default impl`
}

impl<T> Scannable for Result<T, ()> {
    type Ok = T;
    default fn from(r: Result<T, ()>) -> Self { r }
}

fn scan_from_str<T: FromResult>(x: &str) -> T
    where <T as FromResult>::Ok: FromStr  // Doesn't hold for T: FromStr because of the default on T::Ok
{ ... }

// Can also add the FromStr bound to FromResult::Ok, but doesn't help

// Third attempt
trait FromResult<Ok> {
    fn from(r: Result<Ok, ()>) -> Self;
}

impl<T> FromResult<T> for T {
    default fn from(r: Result<Self, ()>) -> Self { ... }
}

impl<T> FromResult<T> for Result<T, ()> {
    fn from(r: Result<T, ())>) -> Self { r }
}


fn scan_from_str<U: FromStr, T: FromResult<U>>(x: &str) -> T { ... }

// Error because we can't infer that U == String
let mut x: Result<String, ()> = scan_from_str("dsfsf");

@tomaka @Aatch

Le problème est que vous n'êtes pas autorisé à vous fier à la valeur d'autres éléments par défaut. Donc, quand vous avez cet impl:

impl<T> ClonableIterator for T where T: Iterator {
    default type ClonableIter = VecIntoIter<T::Item>;

    default fn clonable(self) -> VecIntoIter<T::Item> {
    //                           ^^^^^^^^^^^^^^^^^^^^
        self.collect::<Vec<_>>().into_iter()
    }
}

À l'endroit où j'ai mis en évidence, clonable s'appuie sur Self::ClonableIter , mais comme CloneableIter est déclaré par défaut, vous ne pouvez pas le faire. Le problème est que quelqu'un pourrait se spécialiser et remplacer CloneableIter mais _pas_ clonable .

Nous avions parlé de quelques réponses possibles ici. L'une d'elles était de vous permettre d'utiliser default pour regrouper des éléments où, si vous en remplacez un, vous devez tout remplacer:

impl<T> ClonableIterator for T where T: Iterator {
    default {
        type ClonableIter = VecIntoIter<T::Item>;
        fn clonable(self) -> VecIntoIter<T::Item> { ... }
    }
}

C'est correct, mais un peu "induisant une dérive vers la droite". Le default ressemble également à une portée de nommage, ce qui n'est pas le cas. Il peut y avoir une variante plus simple qui vous permet simplement de basculer entre "override-any" (comme aujourd'hui) et "override-all" (ce dont vous avez besoin).

Nous avions également espéré pouvoir nous en sortir en tirant parti de impl Trait . L'idée serait que cela se produise le plus souvent, comme c'est le cas ici, lorsque vous souhaitez personnaliser le type de retour des méthodes. Alors peut-être que vous pourriez réécrire le trait pour utiliser impl Trait :

pub trait ClonableIterator: Iterator {
    fn clonable(self) -> impl Iterator;
}

Ce serait effectivement une sorte de raccourci une fois implémenté pour un groupe par défaut contenant le type et le fn. (Je ne suis pas sûr qu'il y ait un moyen de le faire uniquement dans l'impl.)

PS, désolé pour le long délai de réponse à vos messages, dont je vois la date du _juillet_.

Bien qu'impl Trait aide, il n'y a pas de RFC qui a été acceptée ou implémentée qui lui permet d'être utilisée avec des corps de trait sous n'importe quelle forme, donc chercher cette RFC semble un peu étrange.

Je suis intéressé par l'implémentation de la fonctionnalité default impl (où tous les éléments sont default ).
Accepteriez-vous une contribution à ce sujet?

@giannicic Certainement! Je serais également ravi d'aider à encadrer le travail.

Existe-t-il actuellement une conclusion sur la question de savoir si les types associés devraient être spécialisables?

Ce qui suit est une simplification de mon cas d'utilisation, démontrant un besoin de types associés spécialisables.
J'ai une structure de données générique, disons Foo , qui coordonne une collection d'objets de trait de conteneur ( &trait::Property ). Le trait trait::Property est implémenté à la fois par Property<T> (soutenu par Vec<T> ) et PropertyBits (soutenu par BitVec , un vecteur de bits).
Dans les méthodes génériques sur Foo , j'aimerais être en mesure de déterminer la bonne structure de données sous-jacente pour T via des types associés, mais cela nécessite une spécialisation pour avoir un impl général pour les cas non spéciaux comme suit.

trait ContainerFor {
    type P: trait::Property;
}

impl<T> ContainerFor for T {
    default type P = Property<T>; // default to the `Vec`-based version
}

impl ContainerFor for bool {
    type P = PropertyBits; // specialize to optimize for space
}

impl Foo {
    fn add<T>(&mut self, name: &str) {
        self.add_trait_obj(name, Box::new(<T as ContainerFor>::P::new())));
    }
    fn get<T>(&mut self, name: &str) -> Option<&<T as ContainerFor>::P> {
        self.get_trait_obj(name).and_then(|prop| prop.downcast::<_>());
    }
}

Merci @aturon !
Fondamentalement, je fais le travail en ajoutant un nouvel attribut "defaultness" à la structure ast::ItemKind::Impl (puis j'utilise le nouvel attribut avec l'attribut impl item "defaultness") mais il y a aussi un rapide et facile
possibilité consistant à mettre par défaut tous les éléments impl du default impl lors de l'analyse.
Pour moi, ce n'est pas une solution "complète" puisque nous avons perdu l'information selon laquelle le "défaut" est lié à l'impl et non à chaque élément de l'impl,
de plus, s'il y a un plan pour introduire un partial impl la première solution fournirait déjà un attribut qui peut être utilisé pour stocker default ainsi que partial . Mais juste pour être sûr et
ne pas perdre de temps, à quoi pensez-vous?

@giannicic @aturon puis-je proposer de créer un numéro spécifique pour discuter de default impl ?

Qu'à cela ne tienne, j'en ai créé un: https://github.com/rust-lang/rust/issues/37653

La règle du treillis me le permettrait-elle, étant donné:

trait Foo {}

trait A {}
trait B {}
trait C {}
// ...

ajoutez des implémentations de Foo pour un sous-ensemble de types qui implémentent une combinaison de A , B , C , ...:

impl Foo for T where T: A { ... }
impl Foo for T where T: B { ... }
impl Foo for T where T: A + B { ... }
impl Foo for T where T: B + C { ... }
// ...

et permettez-moi "d'interdire" certaines combinaisons, par exemple, que A + C ne devrait jamais arriver:

impl Foo for T where T: A + C = delete;

?

Contexte: Je me suis posé cette question lors de l'implémentation d'un trait ApproxEqual(Shape, Shape) pour différents types de formes (points, cubes, polygones, ...) où ce sont tous des traits. J'ai dû contourner ce problème en refactorisant cela en différents traits, par exemple ApproxEqualPoint(Point, Point) , pour éviter des implémentations conflictuelles.

@gnzlbg

et permettez-moi "d'interdire" certaines combinaisons, par exemple, que A + C ne devrait jamais se produire:

Non, ce n'est pas quelque chose que la règle du treillis permettrait. Ce serait plutôt le domaine du «raisonnement négatif» sous une forme ou une autre.

Contexte: Je me suis posé cette question lors de l'implémentation d'un trait ApproxEqual (Shape, Shape) pour différents types de formes (points, cubes, polygones, ...) où ce sont tous des traits. J'ai dû contourner ce problème en refactorisant ceci en différents traits, par exemple, ApproxEqualPoint (Point, Point), pour éviter des implémentations conflictuelles.

Ainsi, @withoutboats a promu l'idée de "groupes d'exclusion", où vous pouvez déclarer qu'un certain ensemble de traits sont mutuellement exclusifs (c'est-à-dire que vous pouvez implémenter au plus l'un d'entre eux). J'imagine cela comme une sorte d'énumération (c'est-à-dire que les traits sont tous déclarés ensemble). J'aime l'idée de cela, d'autant plus que (je pense!) Cela permet d'éviter certains des aspects les plus pernicieux du raisonnement négatif. Mais je pense qu'il faut plus de réflexion sur ce front - et aussi une bonne rédaction qui tente de résumer toutes les «données» qui circulent sur la façon de penser le raisonnement négatif. Peut-être que maintenant que j'ai (principalement) terminé ma série HKT et de spécialisation, je peux y penser ...

@nikomatsakis :

Ainsi, @withoutboats a promu l'idée de "groupes d'exclusion", où vous pouvez déclarer qu'un certain ensemble de traits sont mutuellement exclusifs (c'est-à-dire que vous pouvez implémenter au plus l'un d'entre eux). J'imagine cela comme une sorte d'énumération (c'est-à-dire que les traits sont tous déclarés ensemble). J'aime l'idée de cela, d'autant plus que (je pense!) Cela permet d'éviter certains des aspects les plus pernicieux du raisonnement négatif. Mais je pense qu'il faut plus de réflexion sur ce front - et aussi une bonne rédaction qui tente de résumer toutes les «données» qui circulent sur la façon de penser le raisonnement négatif. Peut-être que maintenant que j'ai (principalement) terminé ma série HKT et de spécialisation, je peux y penser ...

J'ai pensé aux groupes d'exclusions en écrivant ceci (vous l'avez mentionné dans les forums l'autre jour), mais je ne pense pas qu'ils puissent fonctionner car dans cet exemple particulier, toutes les implémentations de traits ne sont pas exclusives. L'exemple le plus trivial est celui des traits Point et Float : un Float _peut_ être un point 1D, donc ApproxEqualPoint(Point, Point) et ApproxEqualFloat(Float, Float) ne peuvent pas être exclusif. Il existe d'autres exemples comme Square et Polygon , ou Box | Cube et AABB (boîte englobante alignée sur l'axe) où la "hiérarchie des traits" nécessite en fait des contraintes plus complexes.

Non, ce n'est pas quelque chose que la règle du treillis permettrait. Ce serait plutôt le domaine du «raisonnement négatif» sous une forme ou une autre.

Je serais au moins capable de mettre en œuvre le cas particulier et d'y mettre un unimplemented!() . Cela suffirait, mais évidemment, j'aimerais plus que le compilateur détecte statiquement les cas dans lesquels j'appelle une fonction avec un unimplemented!() (et à ce stade, nous sommes à nouveau dans un raisonnement négatif) .

La spécialisation treillis pleurer :.

L'idée de «groupes d'exclusion» n'est en réalité que des limites négatives de supertrait. Une chose que nous n'avons pas explorée trop en profondeur est la notion de spécialisation de polarité inversée - vous permettant d'écrire un impl spécialisé qui est de polarité inversée par rapport à son impl moins spécialisé. Par exemple, dans ce cas, vous écririez simplement:

impl<T> !Foo for T where T: A + C { }

Je ne sais pas exactement quelles sont les implications de cette autorisation. Je pense que cela est lié aux problèmes déjà soulignés par Niko sur la façon dont la spécialisation associe en quelque sorte la réutilisation du code au polymorphisme en ce moment.

Avec toute cette discussion sur le raisonnement négatif et les implications négatives, je me sens obligé de revenir sur la vieille idée Haskell des "chaînes d'instances" ( papier , papier , suivi des problèmes GHC , Rust pre-RFC ), comme une source potentielle d'inspiration si rien autre.

Essentiellement, l'idée est que partout où vous pouvez écrire un trait impl, vous pouvez également écrire n'importe quel nombre de clauses "else if" en spécifiant un impl qui devrait s'appliquer au cas où le (s) précédent (s) ne l'a pas fait, avec une "clause else" finale facultative spécifiant un impl négatif (c'est-à-dire que si aucune des clauses de Trait s'applique, alors !Trait s'applique).

@sans bateaux

L'idée de «groupes d'exclusion» n'est en réalité que des limites négatives de supertrait.

Je pense que ce serait suffisant pour mes cas d'utilisation.

Je pense que cela est lié aux problèmes déjà soulignés par Niko sur la façon dont la spécialisation associe en quelque sorte la réutilisation du code au polymorphisme en ce moment.

Je ne sais pas si ceux-ci peuvent être démêlés. Je veux avoir:

  • polymorphisme: un seul trait qui fait abstraction de différentes implémentations d'une opération pour de nombreux types différents,
  • réutilisation du code: au lieu d'implémenter l'opération pour chaque type, je souhaite les implémenter pour des groupes de types qui implémentent certains traits,
  • performance: être capable de remplacer une implémentation déjà existante pour un type particulier ou un sous-ensemble de types qui a un ensemble de contraintes plus spécifique que les implémentations déjà existantes,
  • productivité: être capable d'écrire et de tester mon programme de manière incrémentielle, au lieu d'avoir à ajouter beaucoup de impl s pour le compiler.

Couvrir tous les cas est difficile, mais si le compilateur m'oblige à couvrir tous les cas:

trait Foo {}
trait A {}
trait B {}

impl<T> Foo for T where T: A { ... }
impl<T> Foo for T where T: B { ... }
// impl<T> Foo for T where T: A + B { ... }  //< compiler: need to add this impl!

et me donne également des impls négatifs:

impl<T> !Foo for T where T: A + B { }
impl<T> !Foo for T where T: _ { } // _ => all cases not explicitly covered yet

Je pourrais ajouter incrémentalement des impls au fur et à mesure que j'en ai besoin et obtenir également de belles erreurs de compilateur lorsque j'essaye d'utiliser un trait avec un type pour lequel il n'y a pas d'impl.

Je ne sais pas exactement quelles sont les implications de cette autorisation.

Niko a mentionné qu'il y avait des problèmes de raisonnement négatif. FWIW, la seule chose pour laquelle le raisonnement négatif est utilisé dans l'exemple ci-dessus est de déclarer que l'utilisateur sait qu'un impl pour un cas particulier est nécessaire, mais a explicitement décidé de ne pas en fournir d'implémentation.

Je viens de frapper # 33017 et je ne le vois pas encore lié ici. Il est marqué comme un trou de solidité, il serait donc bon de suivre ici.

Pour https://github.com/dtolnay/quote/issues/7, j'ai besoin de quelque chose de similaire à cet exemple de la RFC qui ne fonctionne pas encore. cc @tomaka @Aatch @rphmeier qui a commenté cela plus tôt.

trait Example {
    type Output;
    fn generate(self) -> Self::Output;
}

impl<T> Example for T {
    default type Output = Box<T>;
    default fn generate(self) -> Box<T> { Box::new(self) }
}

impl Example for bool {
    type Output = bool;
    fn generate(self) -> bool { self }
}

Je suis tombé sur la solution de contournement suivante qui donne un moyen d'exprimer la même chose.

#![feature(specialization)]

use std::fmt::{self, Debug};

///////////////////////////////////////////////////////////////////////////////

trait Example: Output {
    fn generate(self) -> Self::Output;
}

/// In its own trait for reasons, presumably.
trait Output {
    type Output: Debug + Valid<Self>;
}

fn main() {
    // true
    println!("{:?}", Example::generate(true));

    // box("s")
    println!("{:?}", Example::generate("s"));
}

///////////////////////////////////////////////////////////////////////////////

/// Instead of `Box<T>` just so the "{:?}" in main() clearly shows the type.
struct MyBox<T: ?Sized>(Box<T>);

impl<T: ?Sized> Debug for MyBox<T>
    where T: Debug
{
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "box({:?})", self.0)
    }
}

///////////////////////////////////////////////////////////////////////////////

/// Return type of the impl containing `default fn`.
type DefaultOutput<T> = MyBox<T>;

impl Output for bool {
    type Output = bool;
}

impl<T> Example for T where T: Pass {
    default fn generate(self) -> Self::Output {
        T::pass({
            // This is the impl you wish you could write
            MyBox(Box::new(self))
        })
    }
}

impl Example for bool {
    fn generate(self) -> Self::Output {
        self
    }
}

///////////////////////////////////////////////////////////////////////////////
// Magic? Soundness exploit? Who knows?

impl<T: ?Sized> Output for T where T: Debug {
    default type Output = DefaultOutput<T>;
}

trait Valid<T: ?Sized> {
    fn valid(DefaultOutput<T>) -> Self;
}

impl<T: ?Sized> Valid<T> for DefaultOutput<T> {
    fn valid(ret: DefaultOutput<T>) -> Self {
        ret
    }
}

impl<T> Valid<T> for T {
    fn valid(_: DefaultOutput<T>) -> Self {
        unreachable!()
    }
}

trait Pass: Debug {
    fn pass(DefaultOutput<Self>) -> <Self as Output>::Output;
}

impl<T: ?Sized> Pass for T where T: Debug, <T as Output>::Output: Valid<T> {
    fn pass(ret: DefaultOutput<T>) -> <T as Output>::Output {
        <T as Output>::Output::valid(ret)
    }
}

Je travaille toujours sur https://github.com/dtolnay/quote/issues/7 et j'avais besoin d'un motif en diamant. Voici ma solution. cc @zitsen qui a posé des questions à ce sujet plus tôt et @aturon et @rphmeier qui ont répondu.

#![feature(specialization)]

/// Can't have these impls directly:
///
///  - impl<T> Trait for T
///  - impl<T> Trait for T where T: Clone
///  - impl<T> Trait for T where T: Default
///  - impl<T> Trait for T where T: Clone + Default
trait Trait {
    fn print(&self);
}

fn main() {
    struct A;
    A.print(); // "neither"

    #[derive(Clone)]
    struct B;
    B.print(); // "clone"

    #[derive(Default)]
    struct C;
    C.print(); // "default"

    #[derive(Clone, Default)]
    struct D;
    D.print(); // "clone + default"
}

trait IfClone: Clone { fn if_clone(&self); }
trait IfNotClone { fn if_not_clone(&self); }

impl<T> Trait for T {
    default fn print(&self) {
        self.if_not_clone();
    }
}

impl<T> Trait for T where T: Clone {
    fn print(&self) {
        self.if_clone();
    }
}

impl<T> IfClone for T where T: Clone {
    default fn if_clone(&self) {
        self.clone();
        println!("clone");
    }
}

impl<T> IfClone for T where T: Clone + Default {
    fn if_clone(&self) {
        self.clone();
        Self::default();
        println!("clone + default");
    }
}

impl<T> IfNotClone for T {
    default fn if_not_clone(&self) {
        println!("neither");
    }
}

impl<T> IfNotClone for T where T: Default {
    fn if_not_clone(&self) {
        Self::default();
        println!("default");
    }
}

Hit un bug (ou du moins un comportement inattendu de mon point de vue) avec spécialisation et inférence de type: # 38167

Ces deux impls devraient être valides avec spécialisation, non? Il ne semble pas réussir à le ramasser.

impl<T, ST, DB> ToSql<Nullable<ST>, DB> for T where
    T: ToSql<ST, DB>,
    DB: Backend + HasSqlType<ST>,
    ST: NotNull,
{
    ...
}

impl<T, ST, DB> ToSql<Nullable<ST>, DB> for Option<T> where
    T: ToSql<ST, DB>,
    DB: Backend + HasSqlType<ST>,
    ST: NotNull,
{
    ...
}

J'ai déposé https://github.com/rust-lang/rust/issues/38516 pour un comportement inattendu que j'ai rencontré en travaillant sur la création d'une spécialisation dans Serde. Similaire à https://github.com/rust-lang/rust/issues/38167 , c'est un cas où le programme se compile sans l'impl spécialisé et quand il est ajouté, il y a une erreur de type. cc @bluss qui était préoccupé par cette situation plus tôt.

Et si nous permettions la spécialisation sans le mot-clé default dans une seule caisse, de la même manière que nous permettons le raisonnement négatif dans une seule caisse?

Ma principale justification est la suivante: "le modèle d'itérateurs et de vecteurs". Parfois, les utilisateurs veulent implémenter quelque chose pour tous les itérateurs et pour les vecteurs:

impl<I> Foo for I where I: Iterator<Item = u32> { ... }
impl Foo for Vec<u32> { ... }

(Ceci est pertinent pour d'autres situations que les itérateurs et les vecteurs, bien sûr, ce n'est qu'un exemple.)

Aujourd'hui, cela ne se compile pas, et il y a des tsuris et des grincements de dents. La spécialisation résout ce problème:

default impl<I> Foo for I where I: Iterator<Item = u32> { ... }
impl Foo for Vec<u32> { ... }

Mais en résolvant ce problème, vous avez ajouté un contrat public à votre caisse: il est possible de surcharger l'itérateur impl de Foo . Peut-être que nous ne voulons pas vous forcer à faire cela - d'où la spécialisation locale sans default .


La question que je suppose est de savoir quel est exactement le rôle de default . Exiger default était, je pense, à l'origine un geste vers l'explicitation et le code auto-documenté. Tout comme le code Rust est immuable par défaut, privé par défaut, sûr par défaut, il devrait également être définitif par défaut. Cependant, parce que « non-finalité » est une propriété globale, je ne peux pas un élément à moins spécialiser je vous laisse spécialisez un élément.

Exiger default était, je pense, à l'origine un geste vers l'explicitation et le code auto-documenté. Cependant [..] je ne peux pas spécialiser un article à moins que je vous laisse spécialiser un article.

Est-ce vraiment si grave? Si vous voulez spécialiser un impl, alors peut-être que d'autres personnes voudront aussi.

Je m'inquiète parce que le simple fait de penser à cette RFC me donne déjà des flashbacks PTSD sur le travail dans des bases de code C ++ qui utilisent des quantités obscènes de surcharge et d'héritage et qui n'ont aucune idée que wtf se passe dans n'importe quelle ligne de code qui contient un appel de méthode. J'apprécie vraiment les efforts déployés par

Est-ce vraiment si grave? Si vous voulez spécialiser un impl, alors peut-être que d'autres personnes voudront aussi.

Si d'autres personnes veulent "peut-être" se spécialiser aussi, et s'il y a de bons cas où nous ne voudrions pas qu'ils le fassent, nous ne devrions pas rendre impossible de le spécifier. (un peu similaire à l'encapsulation: vous voulez accéder à certaines données et peut-être que d'autres personnes le souhaitent également - vous marquez donc explicitement ces données comme publiques, au lieu de définir par défaut toutes les données comme étant publiques.)

Je m'inquiète parce que le simple fait de penser à cette RFC me donne déjà des flashbacks PTSD ...

Mais comment le refus de cette spécification empêcherait-il ces choses de se produire?

s'il y a de bons cas où nous ne voudrions pas qu'ils le fassent, nous ne devrions pas empêcher de le spécifier.

Ce n'est pas nécessairement une bonne idée de donner un pouvoir aux utilisateurs chaque fois qu'ils peuvent avoir un bon cas d'utilisation pour cela. Pas si cela permet également aux utilisateurs d'écrire du code déroutant.

Mais comment le refus de cette spécification empêcherait-il ces choses de se produire?

Disons que vous voyez foo.bar() et que vous voulez voir ce que fait bar() . À l'heure actuelle, si vous trouvez la méthode implémentée sur un type correspondant et qu'elle n'est pas marquée default vous savez que c'est la définition de méthode que vous recherchez. Avec la proposition de @withoutboats , cela ne sera plus vrai - au lieu de cela, vous ne saurez jamais avec certitude si vous regardez réellement le code qui est exécuté.

au lieu de cela, vous ne saurez jamais avec certitude si vous regardez réellement le code qui est exécuté.

C'est tout à fait une exagération de l'effet de permettre la spécialisation des impls non par défaut pour les types locaux. Si vous regardez un impl concret, vous savez que vous regardez l'implément correct. Et vous avez accès à toute la source de cette caisse; vous pouvez déterminer si cet impl est spécialisé ou pas beaucoup plus tôt que "jamais".

Pendant ce temps, même avec default , le problème persiste lorsqu'un impl n'a pas été finalisé. Si le bon impl est en fait un impl default impl, vous êtes dans la même situation d'avoir du mal à ne pas savoir si c'est le bon impl. Et bien sûr, si la spécialisation est employée, ce sera assez souvent le cas (par exemple, c'est le cas aujourd'hui pour presque chaque impl de ToString ).

En fait, je pense que c'est un problème assez sérieux, mais je ne suis pas convaincu que default résout. Ce dont nous avons besoin, ce sont de meilleurs outils de navigation dans le code. Actuellement, rustdoc fait une approche très efficace en ce qui concerne les impls de traits - il ne lie pas à leur source et ne répertorie même pas les impls fournis par des impls de couverture.

Je ne dis pas que ce changement est un slamdunk en aucun cas, mais je pense que cela mérite une considération plus nuancée.

Ce n'est pas nécessairement une bonne idée de donner un pouvoir aux utilisateurs chaque fois qu'ils peuvent avoir un bon cas d'utilisation pour cela. Pas si cela permet également aux utilisateurs d'écrire du code déroutant.

Exactement, je suis tout à fait d'accord. Je pense que je parle ici d'un "utilisateur" différent, qui est l'utilisateur des caisses que vous écrivez. Vous ne voulez pas qu'ils se spécialisent librement dans votre caisse (affectant peut-être le comportement de votre caisse de manière piratée). D'un autre côté, nous donnerions plus de pouvoir à "l'utilisateur" dont vous parlez, à savoir l'auteur de la caisse, mais même sans la proposition de @withoutboats , vous devrez utiliser "default" et rencontrer le même problème .

Je pense que default aide dans le sens où si vous voulez simplifier la lecture d'un code, vous pouvez demander à personne d'utiliser default ou d'établir des règles de documentation rigoureuses pour l'utiliser. À ce stade, vous n'avez qu'à vous soucier des default s de std , ce que les gens comprendraient probablement mieux.

Je rappelle que l'idée que des règles de documentation pouvaient être imposées aux usages de la spécialisation a contribué à faire approuver le RFC de spécialisation.

@withoutboats ai-je raison de lire votre motivation pour desserrer default car vous voulez une forme restreinte de default qui signifie "remplaçable, mais seulement dans cette caisse" (c'est-à-dire pub(crate) mais pour default )? Cependant, pour garder les choses simples, vous proposez de changer la sémantique de l'omission de default , plutôt que d'ajouter des graduations de default -ness?

Correct. Faire quelque chose comme default(crate) semble exagéré.

A priori, j'imagine que l'on pourrait simuler cela à travers ce que la caisse exporte, non? Y a-t-il des situations où vous ne pourriez pas simplement introduire un trait d'assistance privée avec les méthodes default et l'appeler à partir de votre propre impl s? Vous voulez que l'utilisateur utilise vos default mais n'en fournit aucun?

Correct. Faire quelque chose comme default (crate) semble exagéré.

Je ne suis pas d'accord. Je veux vraiment une forme restreinte de défaut. J'ai eu l'intention de le proposer. Ma motivation est que parfois, les impls d'intersection, etc., vous obligeront à ajouter la valeur par défaut, mais cela ne signifie pas que vous voulez permettre à des caisses arbitraires de changer votre comportement. Désolé, ayez une réunion, je peux essayer d'élaborer avec un exemple dans un peu.

@nikomatsakis J'ai la même motivation, ce que je propose, c'est de supprimer simplement l'exigence par défaut de se spécialiser dans la même caisse, plutôt que d'ajouter plus de leviers. :-)

Si par hasard cette valeur par défaut non exportée pouvait être l'utilisation la plus courante, alors une fonctionnalité #[default_export] serait plus facile à retenir par analogie avec #[macro_export] . Une option intermédiaire peut autoriser cette fonction d'exportation pour les lignes pub use ou pub mod .

Utiliser le mot-clé pub serait mieux, puisque Macros 2.0 supportera les macros comme des éléments normaux et utilisera pub au lieu de #[macro_use] . Utiliser pub pour indiquer la visibilité à tous les niveaux serait une grande victoire pour sa cohérence.

@withoutboats peu importe, je pense que parfois vous voudrez vous spécialiser localement mais pas forcément ouvrir les portes à tous

Il serait préférable d'utiliser le mot-clé pub

Avoir pub default fn signifie "exporter publiquement la valeur par défaut de la fn" au lieu d'affecter la visibilité de la fonction elle-même serait très déroutant pour les nouveaux arrivants.

@jimmycuadra est-ce ce que vous vouliez dire en utilisant le mot-clé pub ? Je suis d'accord avec @sgrif que cela semble plus déroutant, et si nous allons vous permettre de définir explicitement la valeur par défaut, la même syntaxe que nous choisissons pour la visibilité de la portée semble être le bon chemin.

Probablement pas exactement pub default fn , car c'est ambigu, comme vous le dites tous les deux. Je disais simplement qu'il y avait de la valeur à avoir pub signifiant universellement «exposer quelque chose autrement privé à l'extérieur». Il y a probablement une formulation de syntaxe impliquant pub qui serait visuellement différente pour ne pas être confondue avec le fait de rendre la fonction elle-même publique.

Bien que ce soit un peu syntaxique, je ne m'opposerais pas à ce que default(foo) fonctionne comme pub(foo) - la symétrie entre les deux surpasse légèrement la complexité de la syntaxe pour moi.

Avertissement Bikeshed: avons-nous envisagé de l'appeler overridable au lieu de default ? C'est plus littéralement descriptif, et overridable(foo) me lit mieux que default(foo) - ce dernier suggère "c'est la valeur par défaut dans la portée de foo , mais quelque chose d'autre pourrait être la valeur par défaut ailleurs ", alors que le premier dit" ceci est remplaçable dans le cadre de foo ", ce qui est correct.

Je pense que les deux premières questions sont vraiment: l'exportation ou non de default ness est-elle beaucoup plus courante? L'exportation de default ness ne devrait-elle

@burdges Avez-vous les étiquettes «oui cas» et «pas de cas» à l'envers, ou est-ce que je comprends mal ce que vous dites?

Ouais, oups! Fixé!

Nous avons impl<T> Borrow<T> for T where T: ?Sized pour qu'une borne Borrow<T> puisse traiter les valeurs possédées comme si elles étaient empruntées.

Je suppose que nous pourrions utiliser la spécialisation pour optimiser les appels à distance vers clone partir d'un Borrow<T> , oui?

pub trait CloneOrTake<T> {
    fn clone_or_take(self) -> T;
}

impl<B,T> CloneOrTake<T> for B where B: Borrow<T>, T: Clone {
    #[inline]
    default fn clone_or_take(b: B) -> T { b.clone() }
}
impl<T> CloneOrTake<T> for T {
    #[inline]
    fn clone_or_take(b: T) -> T { b };
}

Je pense que cela pourrait rendre Borrow<T> utilisable dans plus de situations. J'ai abandonné la liaison T: ?Sized car on a probablement besoin de Sized lors du retour de T .

Une autre approche pourrait être

pub trait ToOwnedFinal : ToOwned {
    fn to_owned_final(self) -> Self::Owned;
}

impl<B> ToOwnedFinal for B where B: ToOwned {
    #[inline]
    default fn to_owned_final(b: B) -> Self::Owned { b.to_owned() }
}
impl<T> ToOwnedFinal for T {
    #[inline]
    fn to_owned_final(b: T) -> T { b };
}

Nous avons fait des découvertes potentiellement troublantes aujourd'hui, vous pouvez lire les journaux IRC ici: https://botbot.me/mozilla/rust-lang/

Je ne suis pas sûr à 100% de toutes les conclusions auxquelles nous sommes parvenus, d'autant plus que les commentaires de Niko après coup semblent édifiants. Pendant un moment, cela m'a semblé un peu apocalyptique.

Une chose dont je suis assez sûr est que le fait d'exiger le default ne peut pas être rendu compatible avec une garantie que l'ajout de nouveaux impls default est toujours rétrocompatible. Voici la démonstration:

caisse parent v 1.0.0

trait A { }
trait B { }
trait C {
    fn foo(&self);
}

impl<T> C for T where T: B {
    // No default, not specializable!
    fn foo(&self) { panic!() }
}

crate client (dépend de parent )

extern crate parent;

struct Local;

impl parent::A for Local { }
impl parent::C for Local {
    fn foo(&self) { }
}

Local implémente A et C mais pas B . Si local implémentait B , son impl de C serait en conflit avec l'implémentation non spécialisable de C for T where T: B .

caisse parent v 1.1.0

// Same code as before, but add:
default impl<T> B for T where T: A { }

Cet impl a été ajouté et est un impl complètement spécialisable, nous avons donc dit que c'était un changement sans rupture. Cependant , cela crée une implication transitive - nous avions déjà "tout B impl C (non spécialisable)", en ajoutant "tout A impl B (spécialisable)," nous avons implicitement ajouté l'instruction "all A impl C (non spécialisable) ". Désormais, la caisse enfant ne peut pas être mise à niveau.


Il se peut que l'idée de garantir que l'ajout d'impls spécialisables ne soit pas un changement de rupture soit totalement hors de la fenêtre, car Aaron a montré (comme vous pouvez le voir dans les journaux liés ci-dessus) que vous pouvez écrire des impls qui offrent des garanties équivalentes concernant le défaut. . Cependant, les commentaires ultérieurs de Niko suggèrent que de tels impls peuvent être interdits (ou du moins interdits) par les règles orphelines.

Donc, il m'est incertain si la garantie «impls sont insécables» est récupérable, mais il est certain qu'elle n'est pas compatible avec un contrôle explicite sur la finalité implicite.

Y a-t-il un plan pour permettre cela?

struct Foo;

trait Bar {
    fn bar<T: Read>(stream: &T);
}

impl Bar for Foo {
    fn bar<T: Read>(stream: &T) {
        let stream = BufReader::new(stream);

        // Work with stream
    }

    fn bar<T: BufRead>(stream: &T) {
        // Work with stream
    }
}

Donc essentiellement une spécialisation pour une fonction modèle qui a un paramètre de type avec une borne sur A où la version spécialisée a une borne sur B (ce qui nécessite A ).

@torkleyy pas actuellement mais vous pouvez le faire secrètement en créant un trait qui est implémenté à la fois pour T: Read et T: BufRead et contenant les parties de votre code que vous souhaitez spécialiser dans les impls de ce trait. Il n'a même pas besoin d'être visible dans l'API publique.

En ce qui concerne le problème de compatibilité descendante, je pense que grâce aux règles orphelines, nous pouvons nous en sortir avec ces règles:

_Un impl est rétrocompatible à ajouter à moins que : _

  • _Le trait impliqué est un trait automatique._
  • _Le receveur est un paramètre de type, et chaque trait de l'impl existait auparavant.

Autrement dit, je pense que dans tous les exemples problématiques, l'implément ajouté est un impl général. Nous voulions dire que les impls de couverture entièrement par défaut sont également acceptables, mais je pense que nous devons simplement dire que l'ajout d'impls de couverture existants peut être un changement radical.

La question est de savoir quelle garantie voulons-nous faire face à cela - par exemple, je pense que ce serait une très belle propriété si au moins une couverture implicite ne peut être qu'un changement de rupture basé sur le code dans votre caisse, vous pouvez donc revoir votre caisse et sachez avec certitude si vous devez ou non incrémenter la version majeure.

@sans bateaux

En ce qui concerne le problème de compatibilité descendante, je pense que grâce aux règles orphelines, nous pouvons nous en sortir avec ces règles:

_Un impl est rétrocompatible à ajouter à moins que : _

  • _Le trait impliqué est un trait automatique._
  • _Le receveur est un paramètre de type, et chaque trait de l'impl existait auparavant.

Autrement dit, je pense que dans tous les exemples problématiques, l'implément ajouté est un impl général. Nous voulions dire que les impls de couverture entièrement par défaut sont également acceptables, mais je pense que nous devons simplement dire que l'ajout d'impls de couverture existants peut être un changement radical.

Une semaine et de nombreuses discussions plus tard, ce n'est malheureusement pas le cas .

Les résultats que nous avons obtenus sont: crying_cat_face :, mais je pense que ce que j'ai écrit là-bas est le même que votre conclusion. L'ajout de couvertures impls est un changement radical, quoi qu'il arrive. Mais seulement des impls de couverture (et des impls de trait automatique); pour autant que je sache, nous n'avons pas trouvé de cas où un impl non-couverture pourrait briser le code en aval (et ce serait très mauvais).

J'ai pensé à un moment donné que nous pourrions être en mesure d'assouplir les règles orphelines afin que vous puissiez implémenter des traits pour des types tels que Vec<MyType> , mais si nous le faisions, cette situation se déroulerait alors exactement de la même manière:

//crate A

trait Foo { }

// new impl
// impl<T> Foo for Vec<T> { }
// crate B
extern crate A;

use A::Foo;

trait Bar {
    type Assoc;
}

// Sadly, this impl is not an orphan
impl<T> Bar for Vec<T> where Vec<T>: Foo {
    type Assoc = ();
}
// crate C

struct Baz;

// Therefore, this impl must remain an orphan
impl Bar for Vec<Baz> {
    type Assoc = bool;
}

@withoutboats Ah, j'ai compris votre liste à deux ou plutôt que et , ce qui semble être ce que vous vouliez dire?

@aturon Oui, je voulais dire «ou» - ce sont les deux cas où c'est un changement radical. Toute implication de trait automatique, aussi concrète soit-elle, est un changement radical en raison de la façon dont nous permettons au raisonnement négatif à leur sujet de se propager: https://is.gd/k4Xtlp

Autrement dit, à moins qu'il ne contienne de nouveaux noms. AFAIK un impl qui contient un nouveau nom ne se brise jamais.

@withoutboats Je me demande si nous pouvons / devrions restreindre les gens en s'appuyant sur une logique négative autour des traits automatiques. Autrement dit, si nous disons que l'ajout de nouveaux impls de traits automatiques est un changement légal, nous pourrions alors avertir des impls qui pourraient être cassés par une caisse en amont ajoutant Send . Cela fonctionnerait mieux si nous avions:

  1. spécialisation stable, on pourrait surmonter les avertissements en ajoutant default à des endroits stratégiques (la plupart du temps);
  2. une forme d'implémentations négatives explicites, de sorte que des types comme Rc puissent déclarer leur intention de ne jamais être Send - mais alors nous avons ceux pour les traits automatiques, donc nous pourrions les prendre en compte.

Je ne sais pas, je pense que cela dépend de la forte motivation ou non. Il semble particulièrement improbable que vous réalisiez qu'un type pourrait avoir un unsafe impl Send/Sync après l'avoir déjà publié; Je pense que la plupart du temps, ce serait sûr, vous aurez écrit un type avec la prescience que ce serait sûr (parce que c'est le but du type).

J'ajoute tout le temps unsafe impl Send/Sync après coup. Parfois parce que je le sécurise pour les threads, parfois parce que je réalise que l'API C avec laquelle je m'interface peut être partagée entre les threads, et parfois c'est simplement parce que quelque chose doit être Send / Sync isn Ce n'est pas ce à quoi je pense quand j'introduis un type.

Je les ajoute également après coup lors de la liaison des API C - souvent parce que quelqu'un demande explicitement ces limites, alors je vérifie ce que la bibliothèque sous-jacente garantit.

Une chose que je n'aime pas dans la façon dont la spécialisation des traits associés fonctionne actuellement, ce modèle ne fonctionne pas:

trait Buffer: Read {
    type Buffered: BufRead;
    fn buffer(self) -> impl BufRead;
}

impl<T: Read> Buffer for T {
    default type Buffered = BufReader<T>;
    default fn buffer(self) -> BufReader<T> {
        BufReader::new(self)
    }
}

impl<T: BufRead> Buffer for T {
    type Buffered = Self;
    fn buffer(self) -> T {
        self
    }
}

C'est parce que le système actuel exige que cet impl soit valide:

impl Buffer for SomeRead {
    type Buffered = SomeBufRead;
    // no overriding of fn buffer, it no longer returns Self::Buffered
}

impl Trait en traits libérerait beaucoup de désir pour ce type de modèle, mais je me demande s'il n'y a pas de meilleure solution où l'impl générique est valide mais cette spécialisation ne fonctionne pas car elle introduit une erreur de type ?

@withoutboats Ouais, c'est l'une des principales questions non résolues sur le design (que j'avais oublié de soulever lors de discussions récentes). Il y a pas mal de discussions à ce sujet sur le fil RFC original, mais je vais essayer d'écrire un résumé des options / compromis bientôt.

@aturon La solution actuelle est-elle la plus conservatrice (compatible avec tout ce que nous voulons faire) ou est-ce une décision que nous devons prendre avant de stabiliser?

Personnellement, je pense que la seule vraie solution à ce problème soulevé par default . C'est en quelque sorte la solution meilleure est meilleure, mais j'ai l'impression que la variante pire est meilleure (remplacer tout moyen l'emportant sur tout) est un peu pire. (Mais en fait @withoutboats la façon dont vous avez écrit ce code est déroutante. Je pense qu'au lieu d'utiliser impl BufRead comme type de retour de Buffer , vous vouliez dire Self::BufReader , non?)

Dans ce cas, ce qui suit serait autorisé:

trait Buffer: Read {
    type Buffered: BufRead;
    fn buffer(self) -> impl BufRead;
}

impl<T: Read> Buffer for T {
    default {
        type Buffered = BufReader<T>;
        fn buffer(self) -> BufReader<T> {
            BufReader::new(self)
        }
    }
}

impl<T: BufRead> Buffer for T {
    type Buffered = Self;
    fn buffer(self) -> T {
        self
    }
}

Mais peut-être pouvons-nous déduire ces groupements? Je n'y ai pas beaucoup réfléchi, mais il semble que le fait que les valeurs par défaut des éléments soient «intriqués» soit visible à partir de la définition du trait.

Mais en fait @withoutboats la façon dont vous avez écrit ce code est déroutante. Je pense qu'au lieu d'utiliser impl BufRead comme type de retour de Buffer, vous vouliez dire Self :: BufReader, non?

Oui, j'avais modifié la solution en une solution basée sur un trait implicite, puis je suis revenu en arrière, mais j'ai manqué le type de retour dans le trait.

Peut-être que quelque chose comme le système de type de ce langage peut également être intéressant, car il semble être similaire à Rusts, mais avec certaines fonctionnalités, cela peut résoudre les problèmes actuels.
( A <: B serait vrai dans Rust lorsque A est une structure et implémente le trait B , ou lorsque A est un trait, et des implémentations génériques pour les objets de ce trait existe, je pense)

Il semble qu'il y ait un problème avec le trait Display pour la spécialisation.
Par exemple, cet exemple ne compile pas:

use std::fmt::Display;

pub trait Print {
    fn print(&self);
}

impl<T: Display> Print for T {
    default fn print(&self) {
        println!("Value: {}", self);
    }
}

impl Print for () {
    fn print(&self) {
        println!("No value");
    }
}

fn main() {
    "Hello, world!".print();
    ().print();
}

avec l'erreur suivante:

error[E0119]: conflicting implementations of trait `Print` for type `()`:
  --> src/main.rs:41:1
   |
35 |   impl<T: Display> Print for T {
   |  _- starting here...
36 | |     default fn print(&self) {
37 | |         println!("Value: {}", self);
38 | |     }
39 | | }
   | |_- ...ending here: first implementation here
40 | 
41 |   impl Print for () {
   |  _^ starting here...
42 | |     fn print(&self) {
43 | |         println!("No value");
44 | |     }
45 | | }
   | |_^ ...ending here: conflicting implementation for `()`

pendant que cela compile:

pub trait Print {
    fn print(&self);
}

impl<T: Default> Print for T {
    default fn print(&self) {
    }
}

impl Print for () {
    fn print(&self) {
        println!("No value");
    }
}

fn main() {
    "Hello, world!".print();
    ().print();
}

Merci de résoudre ce problème.

@antoyo êtes-vous sûr que c'est parce que Display est spécial, ou est-ce que c'est parce que Display n'est pas implémenté pour les tuples alors que Default est?

@hepmaster
Je ne sais pas s'il s'agit de Display , mais ce qui suit fonctionne avec un trait Custom non implémenté pour les tuples:

pub trait Custom { }

impl<'a> Custom for &'a str { }

pub trait Print {
    fn print(&self);
}

impl<T: Custom> Print for T {
    default fn print(&self) {
    }
}

impl Print for () {
    fn print(&self) {
        println!("No value");
    }
}

fn main() {
    "Hello, world!".print();
    ().print();
}

Au fait, voici la vraie chose que je veux réaliser avec la spécialisation:

pub trait Emit<C, R> {
    fn emit(callback: C, value: Self) -> R;
}

impl<C: Fn(Self) -> R, R, T> Emit<C, R> for T {
    default fn emit(callback: C, value: Self) -> R {
        callback(value)
    }
}

impl<C> Emit<C, C> for () {
    fn emit(callback: C, _value: Self) -> C {
        callback
    }
}

Je veux appeler une fonction par défaut, ou renvoyer une valeur si le paramètre serait unité.
J'obtiens la même erreur sur les implémentations conflictuelles.
Est-il possible (ou est-ce possible) de faire cela avec spécialisation?
Sinon, quelles sont les alternatives?

Edit: Je pense avoir compris pourquoi il ne compile pas:
T en for T est plus général que () en for () donc le premier impl ne peut pas être la spécialisation.
Et C est plus général que C: Fn(Self) -> R donc le deuxième impl ne peut pas être la spécialisation.
Veuillez me dire si je me trompe.
Mais je ne comprends toujours pas pourquoi cela ne fonctionne pas avec le premier exemple avec Display .

C'est actuellement le comportement correct.

Dans l'exemple Custom , ces impls ne se chevauchent pas en raison d'un raisonnement négatif local spécial. Comme le trait provient de cette caisse, nous pouvons en déduire que () , qui n'a pas d'implément de Custom , ne chevauche pas T: Custom . Aucune spécialisation nécessaire.

Cependant, nous n'effectuons pas ce raisonnement négatif pour les traits qui ne proviennent pas de votre caisse. La bibliothèque standard pourrait ajouter Display for () dans la prochaine version, et nous ne voulons pas que ce soit un changement radical. Nous voulons que les bibliothèques aient la liberté d'apporter ce genre de changements. Ainsi, même si () n'implique pas Display, nous ne pouvons pas utiliser ces informations dans la vérification du chevauchement.

Mais aussi, comme () n'implique pas Display, il n'est pas plus spécifique que T: Display . C'est pourquoi la spécialisation ne fonctionne pas, alors que dans le cas par défaut, (): Default , donc cet impl est plus spécifique que T: Default .

Les impls comme celui-ci sont en quelque sorte dans les «limbes» où nous ne pouvons ni supposer qu'ils se chevauchent ou non. Nous essayons de trouver une façon raisonnée de faire en sorte que cela fonctionne, mais ce n'est pas la première implémentation de la spécialisation, c'est une extension rétrocompatible de cette fonctionnalité à venir plus tard.

J'ai déposé le n ° 40582 pour suivre le problème de solidité lié à la durée de vie.

J'ai eu un problème en essayant d'utiliser la spécialisation, je ne pense pas que ce soit tout à fait la même chose que ce que @antoyo avait, je l'avais classé comme un problème séparé # 41140, je peux apporter l'exemple de code ici si nécessaire

@ afonso360 Non, une question distincte convient.

De manière générale: à ce stade, la poursuite des travaux sur la spécialisation est bloquée sur les travaux sur Chalk , ce qui devrait nous permettre de nous attaquer aux problèmes de solidité et est également susceptible d'éclaircir les CIE frappés aujourd'hui.

Quelqu'un peut-il clarifier s'il s'agit d'un bogue ou de quelque chose qui est délibérément interdit? https://is.gd/pBvefi

@sgrif Je pense que le problème ici est simplement que la projection des types associés par défaut est interdite. Les diagnostics pourraient être meilleurs cependant: https://github.com/rust-lang/rust/issues/33481

Pourriez-vous expliquer pourquoi il devrait être interdit? Nous savons qu'aucune implication plus spécifique ne pourrait être ajoutée, car elle violerait les règles orphelines.

Ce commentaire indique qu'il est nécessaire dans certains cas d'exiger de la solidité (même si je ne sais pas pourquoi) et dans d'autres de forcer les consommateurs de l'interface à la traiter comme un type abstrait: https://github.com/rust- lang / rust / blob / e5e664f / src / librustc / traits / project.rs # L41

Quelqu'un a-t-il déjà pu consulter https://github.com/rust-lang/rust/issues/31844#issuecomment -266221638? Ces impls devraient être valables avec spécialisation pour autant que je sache. Je crois qu'il y a un bug qui les empêche.

@sgrif Je pense que le problème avec votre code peut être similaire à celui de https://github.com/rust-lang/rust/issues/31844#issuecomment -284235369 que @withoutboats a expliqué dans https://github.com / rust-lang / rust / issues / 31844 # issuecomment -284268302. Cela étant dit, sur la base du commentaire de

En passant, j'ai essayé de mettre en œuvre ce qui suit, sans succès:

trait Optional<T> {
    fn into_option(self) -> Option<T>;
}

impl<R, T: Into<R>> Optional<R> for T {
    default fn into_option(self) -> Option<R> {
        Some(self.into())
    }
}

impl<R> Optional<R> for Option<R> {
    fn into_option(self) -> Option<R> {
        self
    }
}

Je m'attendais intuitivement à ce que Option<R> soit plus spécifique que <R, T: Into<R>> T , mais bien sûr, rien n'empêche un impl<R> Into<R> for Option<R> dans le futur.

Je ne sais pas pourquoi cela est interdit, cependant. Même si un impl<R> Into<R> for Option<R> était ajouté à l'avenir, je m'attendrais toujours à ce que Rust choisisse l'implémentation non- default , donc pour autant que je puisse voir, autoriser ce code n'a aucune implication sur le transfert. compatibilité.

Dans l'ensemble, je trouve la spécialisation très frustrante à travailler. Presque tout ce que j'attends de travailler ne fonctionne pas. Les seuls cas où j'ai eu du succès avec la spécialisation sont ceux qui sont très simples, comme avoir deux impl qui incluent T where T: A et T where T: A + B . J'ai du mal à faire fonctionner d'autres choses et les messages d'erreur n'indiquent pas pourquoi les tentatives de spécialisation ne fonctionnent pas. Bien sûr, il reste encore du chemin à parcourir, donc je ne m'attends pas à des messages d'erreur très utiles. Mais il semble y avoir pas mal de cas où je m'attends vraiment à ce que quelque chose fonctionne (comme ci-dessus) mais cela ne fonctionne tout simplement pas, et il est actuellement assez difficile pour moi de déterminer si c'est parce que j'ai mal compris ce qui est autorisé (et plus important encore, pourquoi), si quelque chose ne va pas, ou si quelque chose n'a tout simplement pas encore été implémenté. Un bon aperçu de ce qui se passe avec cette fonctionnalité telle quelle serait très utile.

Je ne suis pas sûr que ce soit au bon endroit, mais nous avons rencontré un problème sur le forum des utilisateurs que je voudrais mentionner ici.

Le code suivant (qui est adapté de la RFC ici ) ne compile pas tous les soirs:

#![feature(specialization)]

trait Example {
    type Output;
    fn generate(self) -> Self::Output;
}

default impl<T> Example for T {
    type Output = Box<T>;
    fn generate(self) -> Self::Output { Box::new(self) }
}

impl Example for bool {
    type Output = bool;
    fn generate(self) -> Self::Output { self }
}

Cela ne semble pas vraiment être un problème mais plutôt un problème d'utilisabilité - si un hypothétique impl ne spécialisait defaulti impl de generate serait pas pas de vérification de type.

Lien vers le fil ici

@ burns47 il existe une solution de contournement déroutante mais utile ici: https://github.com/rust-lang/rust/issues/31844#issuecomment -263175793.

@dtolnay Pas tout à fait satisfaisant - et si nous nous spécialisons sur des traits que nous ne possédons pas (et ne pouvons pas modifier)? Nous ne devrions pas avoir besoin de réécrire / refactoriser les définitions de trait pour faire cette IMO.

Quelqu'un peut-il dire si le code du numéro suivant est intentionnellement rejeté? https://github.com/rust-lang/rust/issues/45542

La spécialisation permettrait-elle d'ajouter quelque chose comme ce qui suit à libcore?

impl<T: Ord> Eq for T {}

impl<T: Ord> PartialEq for T {
    default fn eq(&self, other: &Self) -> bool {
        self.cmp(other) == Ordering::Equal
    }
}

impl<T: Ord> PartialOrd for T {
    default fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

De cette façon, vous pouvez implémenter Ord pour votre type personnalisé et mettre en œuvre automatiquement Eq , PartialEq et PartialOrd .

Notez qu'implémenter Ord et dériver simultanément PartialEq ou PartialOrd est dangereux et peut conduire à des bugs très subtils! Avec ces impls par défaut, vous seriez moins tenté de dériver ces traits, donc le problème serait quelque peu atténué.


Alternativement, nous modifions la dérivation pour profiter de la spécialisation. Par exemple, écrire #[derive(PartialOrd)] au-dessus de struct Foo(String) pourrait générer le code suivant:

impl PartialOrd for Foo {
    default fn partial_cmp(&self, other: &Foo) -> Option<Ordering> {
        self.0.partial_cmp(&other.0)
    }
}

impl PartialOrd for Foo where Foo: Ord {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

De cette façon, l'implémentation par défaut est utilisée si Ord n'est pas implémenté. Mais si c'est le cas, alors PartialOrd s'appuie sur Ord . Malheureusement, cela ne compile pas: error[E0119]: conflicting implementations of trait `std::cmp::PartialOrd` for type `Foo`

@stjepang J'espère certainement que des couvertures comme celles-ci pourront être ajoutées - impl<T:Copy> Clone for T aussi.

je pense

impl<T: Ord> PartialEq for T

devrait être

impl<T, U> PartialEq<U> for T where T : PartialOrd<U>

car PartialOrd nécessite PartialEq et peut également le fournir.

À l'heure actuelle, on ne peut pas vraiment utiliser des types associés pour contraindre une spécialisation, à la fois parce qu'ils ne peuvent pas être laissés non spécifiés et parce qu'ils déclenchent une récursivité inopinée . Voir https://github.com/dhardy/rand/issues/18#issuecomment -358147645

Finalement, j'aimerais voir ce que j'appelle des groupes de spécialisation avec la syntaxe proposée par @nikomatsakis ici https://github.com/rust-lang/rust/issues/31844#issuecomment -249355377 et indépendamment par moi. J'aimerai écrire une RFC sur cette proposition plus tard, lorsque nous serons plus près de stabiliser la spécialisation.

Juste au cas où personne ne le verrait, ce billet de blog couvre une proposition visant à rendre la spécialisation sonore face à une expédition à vie.

Comme les fermetures de copies étaient déjà stabilisées dans la version bêta, les développeurs sont plus motivés pour se stabiliser dès maintenant. La raison en est que Fn et FnOnce + Clone représentent deux ensembles de fermetures qui se chevauchent, et dans de nombreux cas, nous devons implémenter des traits pour les deux.

Comprenez simplement que le libellé de la rfc 2132 semble impliquer qu'il n'y a que 5 types de fermetures:

  • FnOnce (a move fermeture avec toutes les variables capturées n'étant ni Copy ni Clone )
  • FnOnce + Clone (a move fermeture avec toutes les variables capturées étant Clone )
  • FnOnce + Copy + Clone (a move fermeture avec toutes les variables capturées étant Copy et donc Clone )
  • FnMut + FnOnce (une fermeture non- move avec des variables capturées mutées)
  • Fn + FnMut + FnOnce + Copy + Clone (une fermeture non- move sans variables capturées mutées)

Donc, si la spécification n'est pas disponible dans un proche avenir, nous devrions peut-être mettre à jour notre définition des traits Fn afin que Fn ne se chevauche pas avec FnOnce + Clone ?

Je comprends que quelqu'un peut déjà implémenter des types spécifiques qui sont Fn sans Copy/Clone , mais cela devrait-il être obsolète? Je pense qu'il y a toujours une meilleure façon de faire la même chose.

Est-ce que ce qui suit est censé être autorisé par spécialisation (notez l'absence de default ) ou est-ce un bug?

#![feature(specialization)]
mod ab {
    pub trait A {
        fn foo_a(&self) { println!("a"); }
    }

    pub trait B {
        fn foo_b(&self) { println!("b"); }
    }

    impl<T: A> B for T {
        fn foo_b(&self) { println!("ab"); }
    }

    impl<T: B> A for T {
        fn foo_a(&self) { println!("ba"); }
    }
}

use ab::B;

struct Foo;

impl B for Foo {}

fn main() {
    Foo.foo_b();
}

sans spécialisation, cela échoue à construire avec:

error[E0119]: conflicting implementations of trait `ab::B` for type `Foo`:
  --> src/main.rs:24:1
   |
11 |     impl<T: A> B for T {
   |     ------------------ first implementation here
...
24 | impl B for Foo {}
   | ^^^^^^^^^^^^^^ conflicting implementation for `Foo`

c'est ça? il n'y a pas d'impl vide dans mon exemple.

@glandium

 impl B for Foo {}

@MoSal mais cela implique "n'est pas vide" puisque B ajoute une méthode avec une implémentation par défaut.

@gnzlbg Il est vide par définition. Rien entre les accolades.


#![feature(specialization)]

use std::borrow::Borrow;

#[derive(Debug)]
struct Bla {
    bla: Vec<Option<i32>>
}

// Why is this a conflict ?
impl From<i32> for Bla {
    fn from(i: i32) -> Self {
        Bla { bla: vec![Some(i)] }
    }
}

impl<B: Borrow<[i32]>> From<B> for Bla {
    default fn from(b: B) -> Self {
        Bla { bla: b.borrow().iter().map(|&i| Some(i)).collect() }
    }
}

fn main() {
    let b : Bla = [1, 2, 3].into();
    println!("{:?}", b);
}

error[E0119]: conflicting implementations of trait `std::convert::From<i32>` for type `Bla`:
  --> src/main.rs:17:1
   |
11 | impl From<i32> for Bla {
   | ---------------------- first implementation here
...
17 | impl<B: Borrow<[i32]>> From<B> for Bla {
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `Bla`
   |
   = note: upstream crates may add new impl of trait `std::borrow::Borrow<[i32]>` for type `i32` in future versions

La spécialisation n'empêcherait-elle pas d'éventuels conflits futurs?

Bonté moi, c'est une fonctionnalité lente! Pas de progrès depuis plus de deux ans, semble-t-il (certainement d'après le post d'origine). L'équipe lang a-t-elle abandonné cela?

@alexreg voir http://aturon.github.io/2018/04/05/sound-specialization/ pour le dernier développement.

@alexreg Il s'avère que la solidité est _hard_. Je crois qu'il y a du travail sur l'idée des "impls toujours applicables" en cours, donc il y a des progrès. Voir https://github.com/rust-lang/rust/pull/49624. De plus, je crois que le groupe de travail Chalk travaille également sur la mise en œuvre de l'idée "toujours applicable impls", mais je ne sais pas jusqu'où cela a été.

Après quelques disputes, il semble qu'il soit possible d'implémenter efficacement les implémentations d'intersection déjà via un hack en utilisant specialization et overlapping_marker_traits .

https://play.rust-lang.org/?gist=cb7244f41c040db41fc447d491031263&version=nightly&mode=debug

J'ai essayé d'écrire une fonction spécialisée récursive pour implémenter un équivalent à ce code C ++:


Code C ++

#include <cassert>
#include <vector>

template<typename T>
size_t count(T elem)
{
    return 1;
}

template<typename T>
size_t count(std::vector<T> vec)
{
    size_t n = 0;
    for (auto elem : vec)
    {
        n += count(elem);
    }
    return n;
}

int main()
{
    auto v1 = std::vector{1, 2, 3};
    assert(count(v1) == 3);

    auto v2 = std::vector{ std::vector{1, 2, 3}, std::vector{4, 5, 6} };
    assert(count(v2) == 6);

    return 0;
}


J'ai essayé ceci:


Code de rouille

#![feature(specialization)]

trait Count {
    fn count(self) -> usize;
}

default impl<T> Count for T {
    fn count(self) -> usize {
        1
    }
}

impl<T> Count for T
where
    T: IntoIterator,
    T::Item: Count,
{
    fn count(self) -> usize {
        let i = self.into_iter();

        i.map(|x| x.count()).sum()
    }
}

fn main() {
    let v = vec![1, 2, 3];
    assert_eq!(v.count(), 3);

    let v = vec![
        vec![1, 2, 3],
        vec![4, 5, 6],
    ];
    assert_eq!(v.count(), 6);
}


Mais je reçois un:

overflow evaluating the requirement `{integer}: Count`

Je ne pense pas que cela devrait arriver car impl<T> Count for T where T::Item: Count ne devrait pas déborder.

EDIT: désolé, je viens de voir que cela était déjà mentionné

@Boiethios Votre cas d'utilisation fonctionne si vous par défaut sur le fn et non sur l'impl:

#![feature(specialization)]

trait Count {
    fn count(self) -> usize;
}

impl<T> Count for T {
    default fn count(self) -> usize {
        1
    }
}

impl<T> Count for T
where
    T: IntoIterator,
    T::Item: Count,
{
    fn count(self) -> usize {
        let i = self.into_iter();

        i.map(|x| x.count()).sum()
    }
}

fn main() {
    let v = vec![1, 2, 3];
    assert_eq!(v.count(), 3);

    let v = vec![vec![1, 2, 3], vec![4, 5, 6]];
    assert_eq!(v.count(), 6);
}

Le trou de solidité n'a-t-il toujours pas été corrigé?

@alexreg je ne pense pas. Voir http://smallcultfollowing.com/babysteps/blog/2018/02/09/maximally-minimal-specialization-always-applicable-impls/

Je suppose que tout le monde se concentre sur l'édition en ce moment ...

D'accord, merci ... il semble que ce problème dure pour toujours, mais assez juste. C'est dur, je sais. Et l'attention est dirigée ailleurs en ce moment malheureusement.

Quelqu'un peut-il expliquer plus concrètement les raisons de ne pas autoriser les projections pour les types associés par défaut dans des cas entièrement monomorphes? J'ai un cas d'utilisation où je voudrais cette fonctionnalité (en particulier, il serait sémantiquement incorrect que le trait soit jamais invoqué avec des types qui ne sont pas entièrement monomorphes), et s'il n'y a pas de problème de solidité, je ne comprends pas complètement pourquoi c'est interdit.

@pythonesque Il y a des discussions sur https://github.com/rust-lang/rust/pull/42411

Ah, je comprends s'il s'avère que la projection interagit mal avec la spécialisation en général. . Et il est en effet vrai que ce que je veux est d'une saveur de «raisonnement négatif» (bien que des traits fermés ne soient pas vraiment suffisants).

Malheureusement, je ne sais pas s'il existe vraiment un moyen de faire ce que je veux sans une telle fonctionnalité: j'aimerais avoir un type associé qui renvoie "True" lorsque deux types transmis implémentant un trait particulier sont syntaxiquement égaux, et "False" quand ils ne le sont pas (avec le cas "False" déclenchant une recherche de trait plus coûteuse qui peut décider s'ils sont "sémantiquement" égaux). La seule vraie alternative me semble être de toujours faire la recherche coûteuse; ce qui est bien en théorie, mais cela peut être beaucoup plus cher.

(Je pourrais contourner ce problème si le trait était destiné à être fermé, en énumérant simplement toutes les paires possibles de constructeurs dans la position de la tête et en les faisant afficher True ou False; mais il est destiné à être ouvert à l'extension en dehors du référentiel, de sorte que Cela ne fonctionnera probablement pas, d'autant plus que les implémentations dans deux référentiels d'utilisateurs différents ne se connaîtront pas nécessairement).

Quoi qu'il en soit, c'est peut-être juste une indication que ce que je veux faire est un mauvais ajustement pour le système de traits et que je devrais passer à un autre mécanisme, comme les macros: P

Et il est en effet vrai que ce que je veux est d'une saveur de «raisonnement négatif» (bien que des traits fermés ne soient pas vraiment suffisants).

Une alternative au raisonnement négatif est d'exiger qu'un type implémente un seul trait d'un ensemble fermé de traits, de sorte que les implémentations avec d'autres traits de l'ensemble ne puissent pas se chevaucher (par exemple T implémente l'un des { Float | Int | Bool | Ptr } ).

Même s'il y avait un moyen d'imposer cela dans Rust (ce qui n'est pas le cas, AFAIK?), Je ne pense pas que cela résoudrait mon problème. Je voudrais que les utilisateurs de différentes caisses puissent implémenter un nombre arbitraire de nouvelles constantes, qui devraient être comparées égales uniquement à elles-mêmes et inégales à toutes les autres constantes définies, y compris celles inconnues au moment de la définition de la caisse. Je ne vois pas comment un ensemble fermé de traits (ou même un ensemble de familles de traits) peut atteindre cet objectif par lui-même: c'est un problème qui ne peut fondamentalement pas être résolu sans regarder directement les types. La raison pour laquelle cela serait réalisable avec les projections par défaut est que vous pouvez tout par défaut sur "ne pas comparer égal", puis implémenter l'égalité de votre nouvelle constante à elle-même dans la caisse dans laquelle vous avez défini la constante, ce qui ne serait pas contraire à l'orphelin. règles parce que tous les types de l'implémentation de trait étaient dans la même caisse. Si je voulais presque une telle règle mais l'égalité, même cela ne fonctionnerait pas, mais l'égalité est assez bonne pour moi :)

Sur présent tous les soirs, cela fonctionne:

trait Foo {}
trait Bar {}

impl<T: Bar> Foo for T {}
impl Foo for () {}

mais même avec une spécialisation et une utilisation nocturne, cela ne permet pas:

#![feature(specialization)]

trait Foo<F> {}
trait Bar<F> {}

default impl<F, T: Bar<F>> Foo<F> for T {}
impl<F> Foo<F> for () {}

Est-ce que cela a une justification ou est-ce un bug?

@rmanoka N'est-ce pas juste les règles orphelines normales? Dans le premier cas, aucune caisse en aval ne pourrait impl Bar for () donc le compilateur le permet, mais dans le second exemple, une caisse en aval pourrait impl Bar<CustomType> for () qui entrerait en conflit avec votre impl par défaut.

@Boscop Dans ce scénario, l'implément par défaut devrait de toute façon être remplacé par celui qui n'est pas par défaut ci-dessous. Par exemple, si j'avais: impl Bar<bool> for () {} ajouté avant les autres impls, alors je m'attendrais à ce que cela fonctionne (selon RFC / expectation). N'est-ce pas correct?

En creusant plus profondément dans le sens du contre-exemple que vous avez mentionné, je me rends compte (ou je crois) que l'exemple satisfait au critère du "toujours applicable" , et peut être en cours d'élaboration.

Ce problème dépend probablement de # 45814.

Existe-t-il des plans pour prendre en charge les limites des traits par défaut qui ne sont pas présentes dans la spécialisation?

Comme exemple pour lequel cela serait très utile, de sorte que vous pouvez facilement composer la gestion de différents types en créant un Struct générique avec Inner arbitraire pour la fonctionnalité qui ne devrait pas être partagée.

#![feature(specialization)]
trait Handler<M> {
    fn handle(&self, m:M);
}

struct Inner;
impl Handler<f64> for Inner {
    fn handle(&self, m : f64) {
        println!("inner got an f64={}", m);
    }
}

struct Struct<T>(T);
impl<T:Handler<M>, M:std::fmt::Debug> Handler<M> for Struct<T> {
    default fn handle(&self, m : M) {
        println!("got something else: {:?}", m);
        self.0.handle(m)
    }
}
impl<T> Handler<String> for Struct<T> {
    fn handle(&self, m : String) {
        println!("got a string={}", m);
    }
}
impl<T> Handler<u32> for Struct<T> {
    fn handle(&self, m : u32) {
        println!("got a u32={}", m);
    }
}

fn main() {
    let s = Struct(Inner);
    s.handle("hello".to_string());
    s.handle(5.0 as f64);
    s.handle(5 as u32);
}

De plus, dans l'exemple ci-dessus, quelque chose d'étrange que j'ai expérimenté - après avoir supprimé le trait lié à l'implément Handler par défaut (et aussi self.0.handle (m)), le code se compile sans problème. Cependant, lorsque vous supprimez l'implémentation pour u32, cela semble casser l'autre déduction de trait:

#![feature(specialization)]
trait Handler<M> {
    fn handle(&self, m:M);
}

struct Struct<T>(T);
impl<T, M:std::fmt::Debug> Handler<M> for Struct<T> {
    default fn handle(&self, m : M) {
        println!("got something else: {:?}", m);
    }
}
impl<T> Handler<String> for Struct<T> {
    fn handle(&self, m : String) {
        println!("got a string={}", m);
    }
}
// impl<T> Handler<u32> for Struct<T> {
//     fn handle(&self, m : u32) {
//         println!("got a u32={}", m);
//     }
// }
fn main() {
    let s = Struct(());
    s.handle("hello".to_string());
    s.handle(5.0 as f64);
}

Même s'il n'y a pas de code appelant le gestionnaire pour u32, la spécialisation n'étant pas là, le code ne se compile pas.

Edit: cela semble être le même que le deuxième problème ("Cependant, lorsque vous supprimez l'implémentation pour u32, cela semble casser l'autre déduction de trait") que Gladdy a mentionné un post.

Avec rustc 1.35.0-nightly (3de010678 2019-04-11), le code suivant donne une erreur:

#![feature(specialization)]
trait MyTrait<T> {
    fn print(&self, parameter: T);
}

struct Message;

impl<T> MyTrait<T> for Message {
    default fn print(&self, parameter: T) {}
}

impl MyTrait<u8> for Message {
    fn print(&self, parameter: u8) {}
}

fn main() {
    let message = Message;
    message.print(1_u16);
}

Erreur:

error[E0308]: mismatched types
  --> src/main.rs:20:19
   |
18 |     message.print(1_u16);
   |                   ^^^^^ expected u8, found u16

Cependant, le code se compile et fonctionne lorsque j'omets le bloc impl MyTrait<u8> :

#![feature(specialization)]
trait MyTrait<T> {
    fn print(&self, parameter: T);
}

struct Message;

impl<T> MyTrait<T> for Message {
    default fn print(&self, parameter: T) {}
}

/*
impl MyTrait<u8> for Message {
    fn print(&self, parameter: u8) {}
}
*/

fn main() {
    let message = Message;
    message.print(1_u16);
}

Est-ce par conception, est-ce parce que l'implémentation est incomplète ou est-ce un bogue?

Aussi, j'aimerais savoir si ce cas d'utilisation pour la spécialisation (implémentation de traits avec des paramètres de type qui se chevauchent pour un seul type concret par opposition à l'implémentation du même trait pour les types qui se chevauchent) sera pris en charge. Lecture de la section «Définition des règles de priorité» dans la RFC 1210, je pense qu'elle serait prise en charge, mais la RFC ne donne pas de tels exemples et je ne sais pas si nous suivons toujours strictement cette RFC.

Signaler une bizarrerie:

trait MyTrait {}
impl<E: std::error::Error> MyTrait for E {}

struct Foo {}
impl MyTrait for Foo {}  // OK

// But this one is conflicting with error message:
//
//   "... note: upstream crates may add new impl of trait `std::error::Error` for type
//    std::boxed::Box<(dyn std::error::Error + 'static)>` in future versions"
//
// impl MyTrait for Box<dyn std::error::Error> {}

Pourquoi Box<dyn std::error::Error> particulier (évitez d'utiliser le mot "spécial") dans ce cas? Même si cela implique std::error::Error dans le futur, le impl MyTrait for Box<dyn std::error::Error> est toujours une spécialisation valide de impl<E: std::error::Error> MyTrait for E , non?

est toujours une spécialisation valable

Dans votre cas, le impl<E: std::error::Error> MyTrait for E ne peut pas être spécialisé, car il n'a pas default méthode

@ bjorn3 Cela semble fonctionner, mais ce n'est pas le cas même si vous ajoutez des méthodes factices

en caisse bar

pub trait Bar {}
impl<B: Bar> Bar for Box<B> {}

En caisse foo

#![feature(specialization)]

use bar::*;

trait Trait {
    fn func(&self) {}
}

impl<E: Bar> Trait for E {
    default fn func(&self) {}
}

struct Foo;
impl Trait for Foo {}  // OK

impl Trait for Box<dyn Bar> {} // Error error[E0119]: conflicting implementations of trait

Notez que si vous changez la caisse bar en

pub trait Bar {}
impl<B: ?Sized + Bar> Bar for Box<B> {}

Puis crate foo compile.

@ bjorn3 Semble que nous n'avons pas besoin d'une méthode default pour la spécialiser ( terrain de jeu ).

@KrishnaSannasi Je ne peux pas reproduire l'erreur "d'implémentations conflictuelles" dans votre exemple ( terrain de jeu ).

Mise à jour: Oh, je vois. Le trait Bar doit provenir d'une caisse en amont pour que l'exemple fonctionne.

@updogliu que votre exemple n'affiche pas de spécialisation car Foo n'implémente pas Error .

Suis-je en train de programmer trop tard ce soir, ou cela ne devrait-il pas provoquer un débordement de pile?

#![feature(specialization)]
use std::fmt::Debug;

trait Print {
    fn print(self);
}

default impl<T> Print for [T; 1] where T: Debug {
    fn print(self) {
        println!("{:?}", self);
    }
}

impl<T> Print for [T; 1] where T: Debug + Clone {
    fn print(self) {
        println!("{:?}", self.clone());
    }
}

fn main() {
    let x = [0u8];
    x.print();
}

Lien Playground

Les blocs default impl gros grains ont toujours fait des choses très étranges pour moi, je suggérerais d'essayer plutôt la syntaxe de spécialisation à grains fins default fn .

EDIT: Lors de la vérification croisée de la RFC, cela est attendu, car default impl ne signifie en fait _pas_ que tous les éléments du bloc impl sont default ed. Je trouve cette sémantique pour le moins surprenante.

Lien Playground

@ HadrienG2 En fait, j'ai toujours utilisé default fn dans ce projet mais cette fois j'ai oublié le mot-clé default et le compilateur a suggéré de l'ajouter au impl . Je n'avais jamais vu le problème de récursivité de la pile auparavant et je ne savais pas s'il était attendu à ce stade. Merci pour la suggestion, default fn fonctionne bien.

En regardant la RFC originale, il y a une section sur la spécialisation des impls inhérents. Est-ce que quelqu'un a donné que j'ai essayé?

L'approche proposée dans la RFC pourrait ne plus fonctionner directement, du moins, pour les méthodes const inhérentes:

// This compiles correctly today:
#![feature(specialization)] 
use std::marker::PhantomData;
struct Foo<T>(PhantomData<T>);
impl<T> Foo<T> {
    default const fn foo() -> Self { Self(PhantomData) }
    // ^^should't default here error?
}
// ----
// Adding this fails:
impl<T: Copy> Foo<T> {
    const fn foo() -> Self { Self(PhantomData) }
}

La RFC originale propose de transformer la méthode en un trait, de l'implémenter pour le type et de spécialiser l'impl. Je suppose que pour les méthodes const fn, ces impls du trait pour le type devraient être const impls.

Pour tous ceux qui rencontrent cela et sont curieux de connaître le statut, il y a eu quelques avancées conceptuelles importantes en 2018:
http://smallcultfollowing.com/babysteps/blog/2018/02/09/maximally-minimal-specialization-always-applicable-impls/
http://aturon.github.io/tech/2018/04/05/sound-specialization/

Plus récemment, le mois dernier, @nikomatsakis a écrit (à titre d'exemple, dans un autre contexte; le mien en gras) que:

il y avait un problème clé [dans la spécialisation] qui n'a jamais été résolu de manière satisfaisante, un problème de solidité technique concernant les durées de vie et les traits [...] Ensuite, [ces deux articles liés ci-dessus]. Il semble que ces idées aient fondamentalement résolu le problème , mais nous avons été occupés entre-temps et n'avons pas eu le temps de faire un suivi.

Cela semble encourageant, même s'il reste clairement du travail à faire.

(Publier ceci parce que j'ai trouvé ce fil il y a quelques semaines et n'avais aucune idée des progrès de l'année dernière, puis je suis tombé plus récemment sur ces messages par accident. Il y a des commentaires ci-dessus qui les mentionnent, mais GitHub rend de plus en plus difficile d'en voir sauf le premier et derniers commentaires sur un long fil de discussion: cry:. Il pourrait être utile que cette mise à jour fasse partie de la description du problème.)

Bonjour à tous! Quelqu'un pourrait-il me dire pourquoi ce cas d'utilisation ne fonctionne pas? Bugs ou comportements attendus?

Comme cet exemple . impl A for i32 est ok, mais impl A for () ne peut pas être compilé en 1.39.0 tous les soirs.

#![feature(specialization)]

trait A {
    fn a();
}

default impl <T: ToString> A for T {
    fn a() {}
}

impl A for i32 {
    fn a() {}
}

impl A for () {
    fn a() {}
}

message de compilation:

error[E0119]: conflicting implementations of trait `A` for type `()`:
  --> src/lib.rs:16:1
   |
8  | default impl <T: ToString> A for T {
   | ---------------------------------- first implementation here
...
16 | impl A for () {
   | ^^^^^^^^^^^^^ conflicting implementation for `()`
   |
   = note: upstream crates may add new impl of trait `std::fmt::Display` for type `()` in future versions

@Hexilee Mettez default sur les méthodes et non sur impl.

Exemple 2 avec @KrishnaSannasi

@zserik oui, je sais. Je ne pense pas qu'il ait encore été mis en œuvre ou qu'il a été abandonné. En tout cas ça ne marche pas maintenant.

Cela ne fonctionne évidemment pas maintenant, mais je pense que cela devrait fonctionner.

Je pose la question ici, car je n'ai pas remarqué que ce sujet revenait nulle part ailleurs - y a-t-il des plans pour default -ify diverses fonctions de bibliothèque standard, de la même manière que nous avons const fonctions From et Into ( impl<T, U: From<T>> Into<U> for T et impl<T> From<T> for T ) rendent difficile l'écriture générique complète From et Into en aval de core , et ce serait bien si je pouvais remplacer ces conversions dans mes propres caisses.

Même si nous autorisons la spécialisation pour From / Into cela n'aiderait pas les impls génériques à cause du problème de treillis.

@KrishnaSannasi Je ne pense pas que ce soit le cas. Par exemple, ce code devrait fonctionner si From et Into étaient spécialisables, mais pas parce qu'ils ne le sont pas:

impl<M: Into<[S; 2]>, S> From<M> for GLVec2<S> {
    fn from(to_array: M) -> GLVec2<S> {
        unimplemented!()
    }
}
impl<M, S> Into<M> for GLVec2<S>
where
    [S; 2]: Into<M>,
{
    fn into(self) -> M {
        unimplemented!()
    }
}

pub struct GLVec2<S> {
    pub x: S,
    pub y: S,
}

Cela fonctionne si vous convertissez From et Into en un trait personnalisé qui n'a pas ces implémentations génériques: https://play.rust-lang.org/?version=stable&mode=debug&edition= 2018 & gist = cc126b016ff62643946aebc6bab88c98

@Osspial Eh bien, si vous essayez de simuler en utilisant un impl par défaut, vous verrez le problème,

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=e5b9da0eeca05d063e2605135a0b5ead

Je vais répéter, changer From/Into impl pour être un impl par défaut dans la bibliothèque standard ne rendra pas possible les impls génériques pour Into . (et cela n'affecte pas les impls génériques de From )

Salut, il y a un bogue sérieux dans l'implémentation actuelle de la spécialisation. Je le qualifie de bug car même si c'était une décision de conception explicite, cela nous empêche d'utiliser l'une des fonctionnalités de spécialisation les plus puissantes, à savoir la possibilité de création de "types opaques" (ce n'est pas un nom formel). Ce modèle est l'un des blocs de construction les plus primitifs dans d'autres langages fournissant des classes de types, comme Haskell ou Scala.

Ce modèle est simple - nous pouvons définir des structures comme WithLabel ou WithID qui ajoutent des champs et des méthodes aux structures sous-jacentes, donc par exemple si nous créons WithLabel<WithID<MyType>> alors nous pourrons pour obtenir id , label et tous les champs / méthodes de MyType également. Malheureusement, avec la mise en œuvre actuelle, ce n'est pas possible.

Voici un exemple de code montrant l'utilisation de ce modèle. Le code commenté ne se compile pas, alors qu'il devrait rendre ce modèle vraiment utile:

#![feature(specialization)]

use std::ops::Deref;
use std::ops::DerefMut;

// =================
// === WithLabel ===
// =================

struct WithLabel<T>(String, T);

pub trait HasLabel {
    fn label(&self) -> &String;
}

impl<T> HasLabel for WithLabel<T> {
    fn label(&self) -> &String { 
        &self.0
    }
}

// THIS SHOULD COMPILE, BUT GETS REJECTED
// impl<T> HasLabel for T
// where T: Deref, <Self as Deref>::Target : HasLabel {
//     default fn label(&self) -> &String { 
//         self.deref().label() 
//     }
// }

impl<T> Deref for WithLabel<T> {
    type Target = T;
    fn deref(&self) -> &Self::Target {
        &self.1
    }
}

// ==============
// === WithID ===
// ==============

struct WithID<T>(i32, T);

pub trait HasID {
    fn id(&self) -> &i32;
}

impl<T> HasID for WithID<T> {
    fn id(&self) -> &i32 { 
        &self.0
    }
}

// THIS SHOULD COMPILE, BUT GETS REJECTED
// impl<T> HasID for T
// where T: Deref, <Self as Deref>::Target : HasID {
//     default fn id(&self) -> &i32 { 
//         self.deref().id() 
//     }
// }

impl<T> Deref for WithID<T> {
    type Target = T;
    fn deref(&self) -> &Self::Target {
        &self.1
    }
}

// =============
// === Usage ===
// =============

struct A(i32);

type X = WithLabel<WithID<A>>;

fn test<T: HasID + HasLabel> (t: T) {
    println!("{:?}", t.label());
    println!("{:?}", t.id());
}

fn main() {
    let v1 = WithLabel("label1".to_string(), WithID(0, A(1)));
    // test(v1); // THIS IS EXAMPLE USE CASE WHICH DOES NOT COMPILE
}

Afin de faire fonctionner la ligne test(v1) , nous devons ajouter un tel trait impl manuellement:

impl<T: HasID> HasID for WithLabel<T> {
    fn id(&self) -> &i32 { 
        self.deref().id()
    }
}

Bien sûr, pour le rendre complet, nous aurions également besoin de rendre ce trait implicite:

impl<T: HasLabel> HasLabel for WithID<T> {
    fn label(&self) -> &String { 
        self.deref().label()
    }
}

Et c'est TRÈS MAUVAIS . Pour seulement 2 types, c'est simple. Cependant, imaginez que nous ayons 10 définitions de types opaques différentes, qui ajoutent différents champs, comme WithID , WithLabel , WithCallback , ... vous le nommez. Avec le comportement actuel des spécialisations, nous aurions besoin de définir ... plus de 1000 implémentations de traits différents! Si le code commenté était accepté, nous n'aurions besoin que de 10 implémentations de trait et l'implémentation de chaque nouveau type ne nécessitera qu'une seule implémentation supplémentaire .

Je ne sais pas comment votre code est lié à la spécialisation. Votre argument (votre code initial se compile mais la ligne test(v1); commentée ne se compile pas sans l'implémentation manuelle que vous présentez) s'applique toujours si la première ligne #![feature(specialization)] est supprimée.

@qnighy Le code devrait être compilé après avoir décommenté les impls HasLabel for T et HasID for T - ils utilisent la spécialisation. Actuellement, ils sont rejetés (essayez de les décommenter dans le code que j'ai fourni!). Cela a-t-il un sens maintenant pour vous? 🙂

Considérons trois instances WithLabel<WithID<A>> , WithID<WithLabel<A>> et WithLabel<WithLabel<A>> . ensuite

  • le premier impl couvre WithLabel<WithID<A>> et WithLabel<WithLabel<A>> .
  • le deuxième impl couvre WithID<WithLabel<A>> et WithLabel<WithLabel<A>> .

Par conséquent, la paire d'impls ne satisfait pas la clause suivante de la RFC :

Pour garantir la cohérence de la spécialisation, nous nous assurerons que pour deux impls I et J qui se chevauchent, nous avons soit I < J ou J < I . Autrement dit, l'un doit être vraiment plus précis que l'autre.

Et c'est aussi un vrai problème dans votre cas car le HasLabel impl de WithLabel<WithLabel<A>> pourrait être interprété de deux manières.

Comment nous pouvons couvrir ce cas est déjà discuté dans la RFC , et la conclusion est:

Les limitations que la règle de treillis aborde sont assez secondaires par rapport aux principaux objectifs de la spécialisation (comme indiqué dans la motivation), et donc, puisque la règle de treillis peut être ajoutée plus tard, la RFC s'en tient à la règle de chaîne simple pour le moment.

@qnighy , merci d'y penser.

Et c'est aussi un vrai problème dans votre cas car l'implication HasLabel de WithLabel<WithLabel<A>> peut être interprétée de deux manières.

Ceci est vrai si nous ne considérons pas le impl<T> HasLabel for WithLabel<T> comme plus spécialisé que impl<T> HasLabel for T pour l'entrée de WithLabel<WithLabel<A>> . La partie de la RFC que vous avez collée couvre en effet cela, cependant, je pense que c'est une limitation sérieuse et je demanderais de réexaminer la prise en charge de ce cas d'utilisation dans la première version de cette extension.

En attendant, je jouais avec negative trait impls car ils peuvent en fait résoudre les points que vous avez abordés. J'ai créé un code qui n'a pas les problèmes que vous décrivez (sauf si je manque quelque chose), cependant, il ne se compile toujours pas. Cette fois, je ne comprends pas d'où viennent les contraintes mentionnées dans l'erreur, car la résolution ne doit pas être ambiguë.

La bonne chose est qu'en fait tout se compile maintenant (y compris les spécialisations) mais pas l'utilisation de test(v1) :

#![feature(specialization)]
#![feature(optin_builtin_traits)]

use std::ops::Deref;
use std::ops::DerefMut;

// =================
// === WithLabel ===
// =================

struct WithLabel<T>(String, T);

auto trait IsNotWithLabel {}
impl<T> !IsNotWithLabel for WithLabel<T> {}

pub trait HasLabel {
    fn label(&self) -> &String;
}

impl<T> HasLabel for WithLabel<T> {
    fn label(&self) -> &String { 
        &self.0
    }
}

impl<T> HasLabel for T
where T: Deref + IsNotWithLabel, <Self as Deref>::Target : HasLabel {
    default fn label(&self) -> &String { 
        self.deref().label() 
    }
}

impl<T> Deref for WithLabel<T> {
    type Target = T;
    fn deref(&self) -> &Self::Target {
        &self.1
    }
}

// ==============
// === WithID ===
// ==============

struct WithID<T>(i32, T);

pub trait HasID {
    fn id(&self) -> &i32;
}

impl<T> HasID for WithID<T> {
    fn id(&self) -> &i32 { 
        &self.0
    }
}

auto trait IsNotWithID {}
impl<T> !IsNotWithID for WithID<T> {}

impl<T> HasID for T
where T: Deref + IsNotWithID, <Self as Deref>::Target : HasID {
    default fn id(&self) -> &i32 { 
        self.deref().id() 
    }
}

impl<T> Deref for WithID<T> {
    type Target = T;
    fn deref(&self) -> &Self::Target {
        &self.1
    }
}

// =============
// === Usage ===
// =============

struct A(i32);

type X = WithLabel<WithID<A>>;

fn test<T: HasID + HasLabel> (t: T) {
    println!("{:?}", t.label());
    println!("{:?}", t.id());
}

fn main() {
    let v1 = WithLabel("label1".to_string(), WithID(0, A(1)));
    test(v1);
}

En attendant, vous pouvez exploiter la RFC1268 overlapping_marker_traits pour permettre le chevauchement des traits non marqueurs, mais ce hack nécessite trois autres traits (un pour passer par des traits marqueurs, deux pour réacquérir des données effacées par spécialisation).

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=b66ee0021db73efaaa5d46edfb4f3990

@qnighy J'ai créé un problème distinct à propos de ce bogue: https://github.com/rust-lang/rust/issues/66041

Ok, je viens de découvrir que auto traits ne sera jamais une solution ici, car (selon https://doc.rust-lang.org/nightly/unstable-book/language-features/optin-builtin-traits. html) ils se propagent à tous les champs d'une structure:

Les traits automatiques, comme Send ou Sync dans la bibliothèque standard, sont des traits de marqueur qui sont automatiquement implémentés pour chaque type, sauf si le type, ou un type qu'il contient, a explicitement désactivé via un impl négatif.

ÉDITER
@qnighy en quelque sorte, j'ai oublié que vous avez fourni un lien vers le terrain de jeu. ❤️ Merci beaucoup pour cela. Cela fonctionne et je suis étonné de voir à quel point cette solution est piratée. Incroyable que nous puissions exprimer cela actuellement et j'espère que cette possibilité ne disparaîtra pas à l'avenir!

Dans une telle situation, overlapping marker traits sont le seul hack que nous pouvons utiliser maintenant, mais je pense qu'il serait bien de permettre à l'avenir une sorte de solution plus simple pour exprimer des types opaques (comme décrit dans mon précédent post: https : //github.com/rust-lang/rust/issues/31844#issuecomment-549023367).

Un exemple très simple (simplification de l' exemple ci -

trait Trait<T> {}
impl<T> Trait<T> for T {}
impl<T> Trait<()> for T {}

Je ne pense pas que cela touche le problème identifié avec les règles de treillis , mais peut-être qu'un solveur trop simpliste pense que c'est le cas?

Sans cela, la mise en œuvre actuelle est inutile pour mes besoins. Si ce qui précède était autorisé, alors je pense qu'il serait également possible d'implémenter From sur les types de wrapper (bien que je ne sois pas sûr de Into ).

Pour tout organisme qui ne le sait pas encore: il y a cette super astuce découverte par dtolnay qui permet d'utiliser une spécialisation (très limitée) sur la rouille stable

Je ne sais pas si cela a déjà été résolu, mais les traits avec des implémentations par défaut pour leurs méthodes doivent être redéfinis juste pour pouvoir être marqués comme default . Exemple;

trait Trait {
    fn test(&self) { println!("default implementation"); }
}

impl<T> Trait for T {
    // violates DRY principle
    default fn test(&self) { println!("default implementation"); }
}

Je propose la syntaxe suivante pour résoudre ce problème (si cela doit être corrigé):

impl<T> Trait for T {
    // delegates to the already existing default implementation
    default fn test(&self);
}

Déplacé au # 68309

@jazzfool Veuillez refiler ceci comme un problème (s'applique généralement à tous ceux qui posent des questions similaires ici) et me

Existe-t-il une approche pour tester la spécialisation? Par exemple, lors de l'écriture d'un test qui vérifie l'exactitude d'une spécialisation, vous devez d'abord savoir si la spécialisation que vous essayez de tester est réellement appliquée au lieu de l'implémentation par défaut.

@ the8472 voulez-vous dire tester le compilateur , ou voulez-vous dire tester dans votre propre code? Vous pouvez certainement écrire des tests unitaires qui se comportent différemment (c'est-à-dire appeler un fn, et voir si vous obtenez la variante spécialisée). Peut-être dites-vous que les deux variantes sont équivalentes, sauf qu'une est censée être plus rapide, et par conséquent, vous ne savez pas comment tester la version que vous obtenez? Dans ce cas, je suis d'accord, je ne sais pas comment vous pouvez tester cela maintenant.

Vous pourriez, je suppose, créer un autre trait avec le même ensemble d'impls, mais où les fns se comportent différemment, juste pour vous rassurer.

Peut-être dites-vous que les deux variantes sont équivalentes, sauf qu'une est censée être plus rapide, et par conséquent, vous ne savez pas comment tester la version que vous obtenez? Dans ce cas, je suis d'accord, je ne sais pas comment vous pouvez tester cela maintenant.

Vous pouvez tester cela à l'aide d'une macro. Je suis un peu rouillé avec mon Rust, mais quelque chose dans ce sens ...

[#cfg(test)]
static mut SPECIALIZATION_TRIGGERED : bool = false;

[#cfg(test)]
macro_rules! specialization_trigger {
    () =>  { SPECIALIZATION_TRIGGERED = true; };
}

[#cfg(not(test))]
macro_rules! specialization_trigger {
    () => {};
}

Puis utilisez specialization_trigger!() dans l'impl spécialisé, et dans les tests, utilisez assert!(SPECIALIZATION_TRIGGERED);

[#cfg(test)]
static mut SPECIALIZATION_TRIGGERED : bool = false;
...

Vous voudrez utiliser thread_local! { static VAR: Cell<bool> = Cell::new(false); } au lieu de static mut car sinon la variable pourrait être définie dans un thread de cas de test et lue par erreur à partir d'un autre thread. N'oubliez pas non plus de réinitialiser la variable au début de chaque test, sinon vous obtiendrez le true du test précédent.

J'ai une question concernant le texte de la RFC, j'espère que c'est un bon endroit à poser.

Dans la section réutilisation , cet exemple est donné:

trait Add<Rhs=Self> {
    type Output;
    fn add(self, rhs: Rhs) -> Self::Output;
    fn add_assign(&mut self, rhs: Rhs);
}

// the `default` qualifier here means (1) not all items are implied
// and (2) those that are can be further specialized
default impl<T: Clone, Rhs> Add<Rhs> for T {
    fn add_assign(&mut self, rhs: Rhs) {
        let tmp = self.clone() + rhs;
        *self = tmp;
    }
}

Je me demande comment cela est censé vérifier le type, étant donné que tmp a le type Self::Output et que rien n'est connu sur ce type associé. Le texte RFC ne semble pas expliquer cela, du moins pas à proximité de l'endroit où l'exemple est donné.

Y a-t-il un mécanisme ici qui est / était censé faire en sorte que cela fonctionne?

Cette valeur par défaut pourrait-elle être contrainte where T: Add<Output = T> ? Ou est-ce une boucle de causalité?

@RalfJung Je suis d'accord que cela semble faux.

J'ai une question sur la procédure: dans quelle mesure ce problème est-il significatif et dans quelle mesure est-il utile pour les gens d'essayer cette fonctionnalité? Si je comprends bien, la mise en œuvre actuelle est malsaine et incomplète et sera probablement complètement remplacée par de la craie ou autre chose. Si c'est vrai, devrions-nous simplement désimplémenter cette fonctionnalité et d'autres (par exemple les GAT) jusqu'à ce qu'elles puissent être correctement refaites?

Veuillez ne pas désimplémenter. Cassé, malsain et incomplet permet encore l'expérimentation.

Si c'est vrai, devrions-nous simplement désimplémenter cette fonctionnalité et d'autres (par exemple les GAT) jusqu'à ce qu'elles puissent être correctement refaites?

Ne le faites pas, PyO3 (bibliothèque de liaisons Python) dépend actuellement de la spécialisation. Voir https://github.com/PyO3/pyo3/issues/210

Une bonne partie des std n'en dépend-elle pas également? Je pensais que je me souvenais avoir vu beaucoup d'implémentations internes spécialisées pour les éléments liés aux vecteurs et aux chaînes. Non pas que cela devrait empêcher la désimplémentation, mais simplement que ce ne serait pas aussi simple que de supprimer les sections pertinentes du vérificateur de type.

@Lucretiel oui, de nombreuses optimisations utiles (notamment autour des itérateurs) dépendent de la spécialisation, donc ce serait une énorme régression de perf pour la mettre en œuvre.

Par exemple, FusedIterator et TrustedLen sont inutiles sans spécialisation.

PyO3 (bibliothèque de liaisons Python) dépend actuellement de la spécialisation

C'est effrayant, à cause des parties "malsaines". La bibliothèque standard avait des bogues de solidité critiques dus à une mauvaise utilisation de la spécialisation. Êtes-vous sûr de ne pas avoir les mêmes bogues? Essayez d'utiliser min_specialization place, nous espérons que c'est au moins moins sain.

Peut-être que specialization devrait recevoir un avertissement similaire à const_generics disant "cette fonctionnalité est incomplète, défectueuse et défectueuse, ne pas utiliser en production ".

de nombreuses optimisations utiles (en particulier autour des itérateurs) dépendent de la spécialisation, donc ce serait une énorme régression de perf de l'implémenter.

Ces jours-ci, ils dépendent de min_specialization (voir par exemple https://github.com/rust-lang/rust/pull/71321), qui a les plus gros trous de solidité bouchés.

@nikomatsakis

Je suis d'accord que cela semble faux.

Une idée de ce qu'est le code prévu? J'ai d'abord pensé que default impl était destiné à définir également type Output = Self; , mais c'est en fait impossible dans la RFC proposée . Alors peut-être que l'intention était d'avoir une liaison Output = T ?

@RalfJung Une chance que min_specialization puisse être documentée? Je pense qu'il est plus risqué d'utiliser une fonctionnalité complètement non documentée sur une caisse que celle qui a des bogues de solidité connus (et peut-être inconnus). Ni l'un ni l'autre n'est bon, mais au moins ce dernier n'est pas que des composants internes du compilateur.

Je n'ai trouvé aucune mention de min_specialization dans ce problème de suivi en dehors du PR # 71321 - et selon le livre Unstable, c'est le problème de suivi pour cette fonctionnalité.

Je ne sais pas grand-chose non plus sur cette fonctionnalité, je viens de voir les correctifs de l'intégrité de libstd. Il a été introduit dans https://github.com/rust-lang/rust/pull/68970, ce qui explique quelques autres choses à ce sujet.

@matthewjasper serait-il judicieux de documenter un peu plus cela et de demander aux utilisateurs nocturnes de feature(specialization) de migrer?

Il semble qu'il devrait y avoir au moins un avertissement. Il semble que cette fonctionnalité soit manifestement cassée et dangereuse à utiliser dans son état actuel.

Je pense que specialization pourrait devenir un synonyme de min_specialization , mais ajoutez une autre fonctionnalité unsound_specialization si nécessaire pour des projets existants, comme PyO3 ou autre. Cela permettrait à quiconque n'utilise que min_specialization efforts considérables, mais n'importe qui d'autre reçoit le message d'erreur et peut rechercher ici le nouveau nom.

@RalfJung

Une idée de ce qu'est le code prévu?

Eh bien, à un moment donné, nous avions envisagé un mode où les valeurs par défaut pourraient s'appuyer les unes sur les autres. J'imagine donc qu'à ce stade, ce qui suit aurait fonctionné:

default impl<T: Clone, Rhs> Add<Rhs> for T {
    type Output = T;

    fn add_assign(&mut self, rhs: Rhs) {
        let tmp = self.clone() + rhs;
        *self = tmp;
    }
}

La mise en garde était que si vous remplacez un membre de impl , vous deviez tous les remplacer. Plus tard, nous nous sommes éloignés de cette idée, puis avons lancé diverses itérations, telles que les «groupes par défaut» (qui fonctionneraient également ici), et n'avons finalement adopté aucune solution car nous pensions que nous pourrions y arriver plus tard une fois que nous aurons traité l'autre, euh, problèmes urgents (cc # 71420).

Ne le faites pas, PyO3 (bibliothèque de liaisons Python) dépend actuellement de la spécialisation. Voir PyO3 / pyo3 # 210

Mainteneur de PyO3 ici - nous sommes favorables à l'abandon de la spécialisation afin de pouvoir accéder à Rust stable. Est-ce que min_specialization sera probablement stabilisé avant que le reste de la spécialisation ne soit terminé?

Je pense qu'il a été question d'essayer de stabiliser la min_specialization lors de la réunion de conception de la langue de planification de l'édition 2021 (c'est sur youtube; désolé, je suis sur mon téléphone, ou j'essaierais de trouver un lien). J'ai oublié ce qu'ils en ont dit

Je pense qu'il a été question d'essayer de stabiliser la min_specialization lors de la réunion de conception de la langue de planification de l'édition 2021 (c'est sur youtube; désolé, je suis sur mon téléphone, ou j'essaierais de trouver un lien). J'ai oublié ce qu'ils en ont dit

Je pense que c'est le bon lien YouTube: https://youtu.be/uDbs_1LXqus
(également sur mon téléphone)

Ouais, c'est ça. Voici un lien vers la discussion spécifique: https://youtu.be/uDbs_1LXqus?t=2073

J'ai utilisé #[min_specialization] dans une bibliothèque expérimentale que je développais donc j'ai pensé partager mes expériences. Le but est d'utiliser la spécialisation dans sa forme la plus simple: avoir des cas étroits avec des implémentations plus rapides que le cas général. En particulier, pour avoir des algorithmes cryptographiques dans le cas général exécutés en temps constant, mais si toutes les entrées sont marquées Public pour avoir une version spécialisée qui s'exécute dans un temps variable plus rapide (car si elles sont publiques, nous ne se soucier des fuites d'informations à leur sujet via le temps d'exécution). De plus, certains algorithmes sont plus rapides selon que le point de la courbe elliptique est normalisé ou non. Pour que cela fonctionne, nous commençons par

#![feature(rustc_attrs, min_specialization)]

Ensuite, si vous devez créer un trait _specialization predicate_ comme expliqué dans la spécialisation minimale maximale, vous marquez la déclaration de trait avec #[rustc_specialization_trait] .

Toute ma spécialisation se fait dans ce fichier et voici un exemple de trait de prédicat de spécialisation.

La fonctionnalité fonctionne et fait exactement ce dont j'ai besoin. Ceci utilise évidemment un marqueur interne rustc et est donc susceptible de se casser sans avertissement.

Le seul commentaire négatif est que je ne pense pas que le mot-clé default sens. Essentiellement, ce que signifie default ce moment est: "cet impl est spécialisable donc interprétez les impls qui couvrent un sous-ensemble de celui-ci comme une spécialisation de celui-ci plutôt qu'un impl en conflit". Le problème est que cela conduit à un code très étrange:

https://github.com/LLFourn/secp256kfun/blob/6766b60c02c99ca24f816801fe876fed79643c3a/secp256kfun/src/op.rs#L196 -L206

Ici le second impl est spécialisé dans le premier mais c'est aussi default . La signification de default semble être perdue. Si vous regardez le reste des impls, il est assez difficile de savoir quels impls sont spécialisés. De plus, quand je faisais un impl erroné qui chevauchait un impl existant, il était souvent difficile de comprendre où je me suis trompé.

Il me semble que ce serait plus simple si tout était spécialisable et lorsque vous spécialisez quelque chose, vous déclarez précisément ce que vous êtes en train de spécialiser. Transformer l'exemple de la RFC en ce que j'avais en tête:

impl<A, T> Extend<A, T> for Vec<A> where T: IntoIterator<Item=A>
{
    // no need for default
    fn extend(&mut self, iterable: T) {
        ...
    }
}

// We declare explicitly which impl we are specializing repeating all type bounds etc
specialize impl<A, T> Extend<A, T> for Vec<A> where T: IntoIterator<Item=A>
    // And then we declare explicitly how we are making this impl narrower with ‘when’.
    // i.e. This impl is like the first except replace all occurances of ‘T’ with ‘&'a [A]’
    when<'a> T = &'a [A]
{
    fn extend(&mut self, iterable: &'a [A]) {
        ...
    }
}

Merci pour les commentaires.

Mon commentaire ici , en particulier le point 6, fournit un cas concret dans la bibliothèque standard où il peut être souhaitable d'avoir une spécialisation qui n'est que partiellement remplaçable: IndexSet aurait besoin d'un type Output distinct car IndexSet pourrait être implémenté sans Index , mais nous ne voulons probablement pas permettre aux deux types de coexister avec différents types Output . Puisque IndexSet pourrait avoir une implémentation par défaut en termes de IndexMut , il serait raisonnable d'autoriser la spécialisation de la méthode index_set sans permettre la spécialisation de Output .

J'ai du mal avec les vidéos, je ne peux donc pas rechercher la vidéo liée, cependant, j'ai une question sur #[min_specialization] . En l'état, il existe un attribut rustc_unsafe_specialization_marker sur des traits comme FusedIterator qui fournissent des conseils d'optimisation, afin qu'ils puissent être spécialisés. @matthewjasper a écrit:

Ce n'est pas judicieux mais nous le permettons à court terme car il ne peut pas provoquer d'utilisation après la libération avec un code purement sûr de la même manière que la spécialisation sur les méthodes de traits.

Je suppose que le plan est de mettre en œuvre la proposition de @aturon et d'ajouter une modalité de spécialisation pour des traits tels que ceux-ci ( where specialize(T: FusedIterator) ). Mais actuellement, il semble que n'importe quel code puisse se spécialiser sur ces traits . S'il est stabilisé tel quel, les gens pourraient écrire des spécialisations stables qui en dépendent, ce qui signifie que ce défaut serait stabilisé.

La spécialisation sur ces traits devrait-elle également être limitée à la bibliothèque standard, alors? La bibliothèque standard tire-t-elle suffisamment avantage de sa capacité à se spécialiser sur eux?

S'il est stabilisé tel quel, les gens pourraient écrire des spécialisations stables qui en dépendent, ce qui signifie que ce défaut serait stabilisé.

Je crois comprendre que min_specialization tel quel n'est pas destiné à la stabilisation.

Je voudrais en second lieu avoir une sorte de marqueur sur la spécialisation des impls. Il y a eu pas mal de cas de code dans rustc et la bibliothèque standard ne fait pas ce à quoi elle ressemble, car il n'y a aucun moyen de savoir que la spécialisation se produit réellement:

Une spécialisation inutile de Copy :
https://github.com/rust-lang/rust/pull/72707/files#diff -3afa644e1d09503658d661130df65f59L1955

Une "spécialisation" qui n'est pas:
https://github.com/rust-lang/rust/pull/71321/files#diff -da456bd3af6d94a9693e625ff7303113L1589

Une implémentation générée par une macro sauf si un indicateur est passé remplaçant un impl par défaut:
https://github.com/rust-lang/rust/pull/73851/files?file-filters%5B%5D=#diff -ebb36dd2ac01b28a3fff54a1382527ddR124

@matthewjasper le dernier lien ne semble pas être lié à un extrait de

Je ne sais pas s'il s'agit d'un objectif explicite, mais AIUI le fait que la spécialisation des impls ne soit pas marquée vous donne un moyen d'éviter de casser les changements sur les impls de couverture. Un nouveau default impl<T> Trait for T n'est pas en conflit avec les impls en aval - ceux-ci deviennent simplement spécialisés.

Cela pourrait-il être un avertissement seulement de l'avoir non marqué?

Il y a eu pas mal de cas de code dans rustc et la bibliothèque standard ne fait pas ce à quoi elle ressemble parce qu'il n'y a aucun moyen de savoir que la spécialisation se produit réellement

Mon expérience avec java est similaire (mais pas exactement analogue). Il peut être difficile de savoir quelle sous-classe d'une classe est en cours d'exécution ...

Nous voudrions aussi un marqueur sur les impls spécialisables, également pour plus de clarté lors de la lecture, non?

Nous pourrions placer les marqueurs aux deux endroits, ce qui améliore les messages d'erreur ou d'avertissement rustc car ils savent maintenant si la spécialisation est souhaitée et peuvent pointer vers l'autre endroit si elle existe.

Si une caisse en amont ajoute un impl, alors, en plus de la simple mise à niveau, une caisse en aval pourrait utiliser des astuces qui permettent de compiler à la fois la nouvelle et l'ancienne version, pas sûr que ce soit bénéfique.

Je pense que le diff est peut-être trop grand pour montrer le changement. Il pointe vers ceci: https://github.com/rust-lang/rust/blob/fb818d4321dee29e1938c002c1ff79b0e7eaadff/src/librustc_span/def_id.rs#L124

Re: Blanket implique, ils brisent de toute façon des changements:

  • Ils peuvent chevaucher partiellement un impl en aval, ce qui n'est pas autorisé
  • La cohérence peut supposer leur non-existence de manière plus subtile (c'est pourquoi les impls de réservation ont été ajoutés en interne)
  • Les impls spécialisés doivent toujours être applicables, ce qui signifie soit:

    • Nous cassons les impls des peuples (ce que fait min_specialization ).

    • Nous leur demandons d'annoter d'une manière ou d'une autre les limites de leurs traits comme étant toujours applicables si nécessaire.

    • Nous apportons implicitement la modification toujours applicable pour eux et introduisons potentiellement de subtils bogues d'exécution lorsque l'implément par défaut s'applique maintenant.

@cuviper en fait, j'ai l'impression qu'il y avait encore des cas extrêmes autour de l'ajout de nouveaux impls de couverture, même avec la spécialisation. Je me souviens que j'essayais de comprendre ce qu'il faudrait pour nous permettre d'ajouter un impl<T: Copy> Clone for T { } imp J'ai écrit ce billet de blog à ce sujet , en tout cas ... mais je ne me souviens plus maintenant quelle était ma conclusion .

Quoi qu'il en soit, nous pourrions en faire un avertissement de charpie pour ne pas avoir d'annotation #[override] .

Cela dit, si nous pouvions demander à l'utilisateur de déclarer les implications qu'il se spécialise (aucune idée de la façon dont nous le ferions), cela simplifierait certaines choses. À l'heure actuelle, le compilateur doit déduire les relations entre impls et c'est toujours un peu délicat.

L'un des éléments en suspens que nous devons faire dans le projet de craie est d'essayer de revenir en arrière et d'expliquer comment la spécialisation devrait y être exprimée.

Il y a eu pas mal de cas de code dans rustc et la bibliothèque standard ne fait pas ce à quoi elle ressemble parce qu'il n'y a aucun moyen de savoir que la spécialisation se produit réellement

Mon expérience avec java est similaire (mais pas exactement analogue). Il peut être difficile de savoir quelle sous-classe d'une classe est en cours d'exécution ...

En mai, j'ai proposé une alternative à la spécialisation sur IRLO qui ne repose pas réellement sur des impls qui se chevauchent, mais permet plutôt à un seul impl à where match sur son paramètre de type:

impl<R, T> AddAssign<R> for T {
    fn add_assign(&mut self, rhs: R) where match T {
        T: AddAssignSpec<R> => self.add_assign(rhs),
        T: Add<R> + Copy => *self = *self + rhs,
        T: Add<R> + Clone => { let tmp = self.clone() + rhs; *self = tmp; }
    }
}

Les caisses en aval peuvent alors utiliser un tel impl pour implémenter la "spécialisation", car par convention, un tel impl pour le trait Trait correspondrait d'abord aux types qui implémentent un autre trait TraitSpec , et les types en aval serait capable d'implémenter ce trait pour remplacer le comportement générique:

// Crate upstream
pub trait Foo { fn foo(); }
pub trait FooSpec { fn foo(); }

impl<T> Foo for T {
    fn foo() where T {
        T : FooSpec => T::foo(),
        _ => { println!("generic implementation") }
    }
}

fn foo<T : Foo>(t: T) {
    T::foo()
}

// crate downstream
struct A {}
struct B {}

impl upstream::FooSpec for A {
    fn foo() { println!("Specialized"); }
}

fn main() {
    upstream::foo(A); // prints "specialized"
    upstream::foo(B); // prints "generic"
}

Cette formulation donne plus de contrôle à l'amont pour choisir l'ordre des impls applicables, et comme cela fait partie de la signature trait / fonction, cela apparaîtrait dans la documentation. OMI cela empêche "impl chasing" pour savoir quelle branche est réellement applicable, car l'ordre de résolution est explicite.

Cela pourrait peut-être rendre les erreurs autour des durées de vie et de l'égalité des types plus apparentes car seul l'amont pourrait les rencontrer lors de l'implémentation de la spécialisation (puisque l'aval ne met en œuvre qu'un "trait de spécialisation".

Les inconvénients de cette formulation sont qu'il s'agit d'une route très différente de celle de la RFC, et qu'elle est mise en œuvre depuis 2016, et qu'au moins certaines personnes sur le fil ont exprimé des inquiétudes sur le fait que ce ne serait pas aussi expressif et / ou intuitif que l'actuel fonction de spécialisation (je trouve "matching on types" assez intuitif, mais je suis biaisé car je propose la formulation).

La syntaxe de correspondance pourrait avoir un autre avantage (syntaxique): si elle était à un moment donné dans le futur étendue avec des protections de correspondance évaluées par const, alors on n'aurait pas besoin de faire de la gymnastique de type pour exprimer des limites conditionnelles sur des expressions const. Par exemple, on pourrait appliquer des spécialisations basées sur size_of , align_of , needs_drop ou des tailles de tableau.

@dureuill merci pour l'info! C'est en effet une idée intéressante. Une préoccupation que j'ai est que cela ne résout pas nécessairement certains des autres cas d'utilisation prévus pour la spécialisation, en particulier le cas du «comportement de raffinage incrémentiel» tel que décrit par @aturon dans ce billet de blog . Pourtant, cela vaut la peine de garder à l'esprit.

@dureuill L'idée est en effet intéressante et peut avoir beaucoup de potentiel, mais l'alternative n'est pas toujours un échange équivalent.
La raison pour laquelle je ne pense pas que ce soit le cas, c'est qu'on n'a pas la possibilité de remplacer complètement la mise en œuvre plus générale. Un autre problème pourrait également être le fait que nous ne prenons pas en charge toutes les fonctionnalités présentes dans la RFC de syntaxe where dont dépend votre suggestion.
La suggestion est intrigante, alors peut-être qu'elle pourrait avoir sa propre RFC comme une fonctionnalité distincte plutôt que comme un concurrent à la spécialisation parce que les deux seraient utiles et je ne vois aucune raison pour laquelle ils ne peuvent pas vivre ensemble.

@ the8472 @nikomatsakis , @ Dark-Legion: Merci pour les commentaires positifs! J'essaie de répondre à certaines de vos remarques dans le fil de discussion IRLO , car je ne veux pas être trop bruyant sur le problème du suivi (je suis désolé pour chacun d'entre vous qui attendait des nouvelles sur la spécialisation et qui vient de trouver mes divagations: vidées :).

Je peux ouvrir un RFC séparé si j'arrive à écrire quelque chose de publiable. En attendant, je suis très ouvert aux commentaires sur le fil IRLO lié. J'ai ajouté un exemple plus long du billet de blog d'aturon, alors n'hésitez pas à commenter ça!

Je suis également en faveur d'avoir une sorte de marqueur sur la spécialisation des impls.

L'édition 2021 approche, ce qui nous permet de réserver d'autres mots-clés (comme specialize ). En regardant la complexité et l'historique de cette fonctionnalité, je ne pense pas qu'elle se stabilisera avant la sortie de l'édition 2021 (n'hésitez pas à me prouver le contraire), ce qui signifie - à mon avis - jouer avec (un) nouveau (s) mot (s) ) est raisonnable.

Sinon, le seul mot - super ?

Résumé en réutilisant l'exemple de @LLFourn de https://github.com/rust-lang/rust/issues/31844#issuecomment -639977601:

  • super (déjà réservé, mais il pourrait aussi être mal interprété comme une alternative à default )
super impl<A, T> Extend<A, T> for Vec<A> where T: IntoIterator<Item=A>
  • specialize
specialize impl<A, T> Extend<A, T> for Vec<A> where T: IntoIterator<Item=A>
  • spec (abréviation de specialize comme impl est pour implement ) ( préoccupation valide soulevée par @ssokolow dans https://github.com/rust-lang / rust / issues / 31844 # issuecomment-690980762)
spec impl<A, T> Extend<A, T> for Vec<A> where T: IntoIterator<Item=A>
  • override (déjà réservé, merci @ the8472 https://github.com/rust-lang/rust/issues/31844#issuecomment-691042082)
override impl<A, T> Extend<A, T> for Vec<A> where T: IntoIterator<Item=A>

les mots-clés déjà réservés se trouvent ici

ou spec (court pour specialize comme impl est pour implement )

"spec" est déjà plus familier aux gens en tant que raccourci pour "spécification" (par exemple "La spécification HTML 5") donc je ne pense pas que ce serait un bon raccourci pour "spécialiser".

override est un mot-clé réservé, je suppose qu'il était destiné aux fonctions, donc il pourrait être utilisable pour un bloc impl.

specialize Dépend également des paramètres régionaux - en tant qu'Australien, il est spécialisé pour moi, donc utiliser 'spec' supprime l'ambiguïté locale.

specialize Dépend également des paramètres régionaux - en tant qu'Australien, il est spécialisé pour moi, donc utiliser 'spec' supprime l'ambiguïté locale.

cela fonctionne, sauf que 'spec' est une abréviation courante pour la spécification, donc je pense qu'utiliser 'spec' pour signifier la spécialisation serait déroutant. Même si les mots sont orthographiés différemment en Australie, tout le monde peut toujours comprendre quel mot est destiné s'il est orthographié avec un «z» ou un «s».

En tant que Canadien, je dois dire que se spécialiser / se spécialiser n'est pas le seul mot utilisé dans la programmation qui varie selon les paramètres régionaux.

Ici, nous utilisons "couleur", mais cela me fait toujours trébucher dans les rares occasions où un langage de programmation ou une bibliothèque utilise cela au lieu de "couleur". Pour le meilleur ou pour le pire, l'anglais américain est un peu un standard de facto dans la conception d'API et, avec des mots comme couleur / couleur, choisir de privilégier une orthographe par rapport à l'autre est inévitable sans être vraiment artificiel.

Étant donné à quel point je m'attends à ce que «spec» signifie «spécification», je pense que c'est une autre situation où nous devrions simplement considérer l'orthographe anglaise américaine comme la moins mauvaise option.

C'est peut-être le defacto, mais cela ne signifie pas que vous pouvez les utiliser. Je me retrouve à faire des importations comme "utiliser la couleur comme couleur" par exemple. Je trébuche toujours aussi sur le s vs z. Je pense que compte tenu de l'attitude positive de Rust envers l'inclusivité et l'accessibilité, il est logique de choisir des termes linguistiques qui ne dépendent pas des paramètres régionaux, car les petites frustrations des utilisateurs comme la couleur / couleur et s / z s'accumulent.

Je suis d'accord en principe. Je suis juste sceptique que, dans ce cas, il existe un choix neutre au niveau local qui ne pose pas plus de problèmes qu'il n'en résout.

En tant que locuteur natif non anglais, je trouve quelque peu amusant que des anglophones se plaignent d'un supplément de u comme étant un obstacle à l'inclusivité. Imaginez ce que ce serait si tout n'était pas écrit un peu bizarre, mais écrit dans une langue entièrement différente.

En d'autres termes: chaque terme utilisé dans Rust dépend des paramètres régionaux.

Pour le meilleur ou pour le pire, les éléments de Rust sont écrits en anglais américain. Pour beaucoup ici, cela signifie travailler dans leur deuxième ou troisième langue; pour d'autres, cela signifie avoir à ajuster un peu l'orthographe. C'est ce qu'il faut pour que tout un groupe de personnes travaille ensemble. Je pense que l'avantage d'essayer de choisir des mots qui sont orthographiés de la même manière dans de nombreuses variantes de l'anglais est marginal par rapport à choisir un bon terme sans ambiguïté - et spec est ambigu comme indiqué ci-dessus.

utiliser special comme mot-clé?

Alternative, créez deux mots-clés: specialize et specialise et rendez-les équivalents ...

(Ou vous, non-Américains drôles, pouvez apprendre à épeler vraiment correctement: nous: 😂)

Je ne peux pas parler de ce que font la plupart des langues, mais CSS utilise des orthographes en anglais américain pour tout ce qui est nouveau. L'anglais américain semble également être utilisé plus fréquemment dans la programmation.

@ mark-im Malheureusement, c'est une pente glissante qui mène à des arguments selon lesquels Rust devrait avoir des ensembles de mots-clés alternatifs dans toutes les principales langues dont les apprenants pourraient provenir.

Cela complique également inutilement le langage d'avoir plusieurs synonymes pour les mots-clés, car les personnes et les analyseurs ne sont habitués qu'à l'idée que les espaces peuvent varier comme ça.

(Sans oublier que cela déclencherait potentiellement une poussée pour des synonymes équivalents dans les bibliothèques, ce qui nécessiterait alors un travail de conception et de mise en œuvre de rustdoc pour les empêcher d'être un net négatif.)

Plutôt que de discuter du dialecte de l'anglais dans lequel nous voulons que cet identifiant soit, peut-être pouvons-nous faire un compromis et le mettre en hébreu à

@ssokolow alors que l'argument de la pente glissante en général n'est pas un argument fort à utiliser, je suis d'accord avec vous dans ce cas. On pourrait soutenir que plusieurs langues sont bien, mais il y a au moins deux raisons pour lesquelles ce n'est pas le cas:

  • Certains mots dans différentes langues se ressemblent mais signifient des choses différentes (impossible de trouver un exemple lié à la programmation pour le moment, mais un exemple aléatoire: a en slovaque est and en anglais)
  • Les gens auront beaucoup de mal à lire le code dans une autre langue même s'ils connaissent la langue . Je sais par expérience. (En bref: j'ai eu un énorme problème à comprendre certains textes avec des termes directement traduits de l'anglais vers ma "langue maternelle" lors d'une escroquerie universitaire .)

Maintenant en marche arrière, pourquoi différents dialectes de l'anglais devraient-ils être préférés sinon d'autres langues? Je ne vois pas un point. La cohérence (tout est en anglais américain) semble la plus simple, la plus facile à comprendre et la moins sujette aux erreurs.

Tout cela étant dit, je serais très content de "veux-tu dire XXX?" approche de message d'erreur. Les mots neutres qui n'ont pas d'autres problèmes conviennent également.

Au moins, personne n'a besoin de discuter du football dans le code. ;)

Environ 70% des anglophones natifs vivent dans des pays utilisant l'orthographe américaine.

Également..

«L'orthographe -ize est souvent considérée à tort comme un américanisme en Grande-Bretagne. Elle est utilisée depuis le XVe siècle, antérieure à -ise de plus d'un siècle. -Ize vient directement du grec -ιζειν -izein et du latin -izāre, tandis que - ise est disponible via le français -iser. L'Oxford English Dictionary (OED) recommande -ize et répertorie la forme -ise comme alternative. "

"Les publications d'Oxford University Press (OUP) - telles que A Dictionary of Modern English Usage, Hart's Rules et The Oxford Guide to English Usage de Henry Watson Fowler - recommandent également -ize. être acceptable partout sauf aux États-Unis. "

réf. https://en.wikipedia.org/wiki/American_and_British_English_spelling_differences# -ise, _- ize _ (- isation, _- ization)

Il semble que l'espagnol et l'italien en ont un ou deux, alors vous ne savez pas où le français obtient - peut-être l'allemand?

Est-il possible de déplacer davantage de bikeshedding autour de mots-clés et de noms spécifiques vers un fil de discussion interne ? Je suis ce numéro pour les mises à jour de progression de la fonctionnalité, et cette discussion commence à devenir un peu bruyante.

Cette page vous a été utile?
0 / 5 - 0 notes