Rust: Problème de suivi pour `ops::Try` (fonctionnalité `try_trait`)

Créé le 31 mai 2017  ·  99Commentaires  ·  Source: rust-lang/rust

Le trait Try de https://github.com/rust-lang/rfcs/pull/1859; implémenté dans PR https://github.com/rust-lang/rust/pull/42275.

Séparer de https://github.com/rust-lang/rust/issues/31436 pour plus de clarté (par https://github.com/rust-lang/rust/pull/42275#discussion_r119167966)

  • [ ] Stabiliser cela permettra aux utilisateurs de mettre en œuvre Iterator::try_fold

    • [ ] Dans le cadre de la stabilisation, rouvrir le PR n° 62606 pour documenter la mise en œuvre de try_fold pour les itérateurs

    • [ ] Assurez-vous que les implémentations par défaut d'autres éléments ont le DAG à long terme souhaité, car leur modification est pratiquement impossible par la suite. (Plus précisément, ce serait bien que fold soit implémenté en termes de try_fold , afin que les deux n'aient pas besoin d'être remplacés.)

A-error-handling B-RFC-implemented B-unstable C-tracking-issue Libs-Tracked T-lang T-libs

Commentaire le plus utile

Quel est l'état actuel de cette fonctionnalité ?

Tous les 99 commentaires

Quelques pièces de bikeshedding :

  • Avons-nous une motivation particulière pour appeler le type associé Error au lieu de Err ? L'appeler Err ferait s'aligner avec Result : l'autre s'appelle déjà Ok .

  • Avons-nous une motivation particulière pour avoir des méthodes distinctes from_error et from_ok , au lieu d'une seule from_result qui serait plus symétrique avec into_result qui est l'autre la moitié du trait ?

( version mise à jour du lien de jeu de la RFC )

@glaebhoerl

  • Error vs Err été discuté concernant TryFrom dans https://github.com/rust-lang/rust/issues/33417#issuecomment -269108968 et https:// github.com/rust-lang/rust/pull/40281; Je suppose que le nom a été choisi ici pour des raisons similaires.
  • Je crois qu'ils sont séparés parce qu'ils ont des utilisations différentes, et je pense qu'il est rare que quelqu'un ait réellement un Result qu'il essaie de transformer en T:Try . Je préfère Try::from_ok et Try::from_error à toujours appeler Try::from_result(Ok( et Try::from_result(Err( , et je suis heureux d'implémenter les deux méthodes plutôt que d'écrire la correspondance. C'est peut-être parce que je pense à into_result non pas comme Into<Result> , mais comme "était-ce une réussite ou un échec ?", le type spécifique étant Result comme détail d'implémentation sans importance. (Je ne veux pas suggérer ou rouvrir le "il devrait y avoir un nouveau type pour produire-valeur vs retour anticipé, cependant.) Et pour la documentation, j'aime que from_error parle de ? (ou éventuellement throw ), tandis que from_ok parle de succès-emballage (#41414), plutôt que de les avoir tous les deux dans la même méthode.

Je ne sais pas si c'est le bon forum pour ce commentaire, merci de me rediriger si ce n'est pas le cas : smiley:. Cela aurait peut-être dû être sur https://github.com/rust-lang/rfcs/pull/1859 et j'ai raté la période de commentaires ; Oups!


Je me demandais s'il était justifié de diviser le trait Try ou de supprimer la méthode into_result ; pour moi actuellement Try est un peu comme la somme des traits FromResult (contenant from_error et from_ok ) et IntoResult (contenant into_result ).

FromResult permet une sortie anticipée très ergonomique avec l'opérateur ? , qui, je pense, est le cas d'utilisation qui tue pour la fonctionnalité. Je pense que IntoResult peut déjà être implémenté proprement avec des méthodes par cas d'utilisation ou comme Into<Result> ; est-ce que je manque quelques exemples utiles?

Suite au Try trait RFC , expr? pourrait se transformer en :

match expr { // Removed `Try::into_result()` here.
    Ok(v) => v,
    Err(e) => return Try::from_error(From::from(e)),
}

Les exemples motivants que j'ai considérés sont Future et Option .

Futur

Nous pouvons implémenter FromResult naturellement pour struct FutureResult: Future comme :

impl<T, E> FromResult for FutureResult {
    type Ok = T;
    type Error = E;
    fn from_error(v: Self::Error) -> Self {
        future::err(v)
    }
    fn from_ok(v: Self::Ok) -> Self {
        future::ok(v)
    }
}

En supposant une implémentation raisonnable pour ? qui reviendra d'une fonction valorisée impl Future lorsqu'elle est appliquée à un Result::Err alors je peux écrire :

fn async_stuff() -> impl Future<V, E> {
    let t = fetch_t();
    t.and_then(|t_val| {
        let u: Result<U, E> = calc(t_val);
        async_2(u?)
    })
}
fn fetch_t() -> impl Future<T, E> {}
fn calc(t: T) -> Result<U,E> {}

C'est exactement ce que j'essayais de mettre en œuvre plus tôt dans la journée et Try parvient ! Mais si nous essayons d'implémenter le Try actuel pour Future il n'y a peut-être pas de choix canonique pour into_result ; il peut être utile de paniquer, de bloquer ou d'interroger une fois, mais aucun de ceux-ci ne semble universellement utile. S'il n'y avait pas de into_result sur Try je peux implémenter une sortie anticipée comme ci-dessus, et si je dois convertir un Future en Result (et de là en n'importe quel Try ) Je peux le convertir avec une méthode appropriée (appelez la méthode wait pour bloquer, appelez certains poll_once() -> Result<T,E> , etc.).

Option

Option est un cas similaire. Nous implémentons from_ok , from_err naturellement comme dans le trait RFC Try , et pourrions convertir Option<T> en Result<T, Missing> simplement avec o.ok_or(Missing) ou une méthode pratique ok_or_missing .


J'espère que cela pourra aider!

Je suis peut-être très en retard pour tout cela, mais il m'est venu à l'esprit ce week-end que ? a une sémantique plutôt naturelle dans les cas où vous voudriez court-circuiter sur _success_.

fn fun() -> SearchResult<Socks> {
    search_drawer()?;
    search_wardrobe()
}

Mais dans ce cas, le nom des méthodes de traits Try ne convient pas.

Cela élargirait cependant la signification de l'opérateur ? .

En prototypant un try_fold pour rayon , je me suis retrouvé à vouloir quelque chose comme Try::is_error(&self) pour les méthodes Consumer::full() . (Rayon a ceci parce que les consommateurs sont essentiellement des itérateurs de style push, mais nous voulons toujours court-circuiter les erreurs.) J'ai plutôt dû stocker les valeurs intermédiaires sous la forme Result<T::Ok, T::Err> afin que je puisse appeler Result::is_err() .

A partir de là je souhaitais aussi un Try::from_result pour revenir à T à la fin, mais un match mapping vers T::from_ok et T::from_error n'est pas trop mauvais.

Le trait Try peut fournir une méthode from_result pour l'ergonomie si from_ok et from_error sont requis. Ou vice versa. D'après les exemples donnés, je vois un cas pour offrir les deux.

Étant donné que le PR #42275 a été fusionné, cela signifie-t-il que ce problème a été résolu ?

@skade Notez qu'un type peut définir celui qui court-circuite, comme LoopState fait pour certains Iterator internes. Ce serait peut-être plus naturel si Try parlait de "continuer ou de rompre" au lieu de réussir ou d'échouer.

@cuviper La version dont j'avais besoin était soit le type Ok soit le type Error -in-original. J'espère que déstructurer et reconstruire est une chose assez générale pour qu'elle s'optimise bien et qu'un tas de méthodes spéciales sur le trait ne seront pas nécessaires.

@ErichDonGubler Il s'agit d'un problème de suivi, il n'est donc pas résolu tant que le code correspondant n'est pas stable.

Rapport d'expérience :

J'ai été un peu frustré d'essayer de mettre ce trait en pratique. Plusieurs fois maintenant, j'ai été tenté de définir ma propre variante sur Result pour une raison quelconque, mais à chaque fois, j'ai fini par n'utiliser que Result à la fin, principalement à cause de la mise en œuvre de Try était trop ennuyeux. Je ne suis pas tout à

Exemple:

Dans le nouveau solveur pour Chalk VM, je voulais avoir une énumération qui indique le résultat de la résolution d'un "brin". Cela avait quatre possibilités :

enum StrandFail<T> {
    Success(T),
    NoSolution,
    QuantumExceeded,
    Cycle(Strand, Minimums),
}

Je voulais que ? , lorsqu'il est appliqué à cette énumération, déballe le "succès", mais propage tous les autres échecs vers le haut. Cependant, afin d'implémenter le trait Try , j'aurais dû définir une sorte de type "résiduel" qui encapsule uniquement les cas d'erreur :

enum StrandFail {
    NoSolution,
    QuantumExceeded,
    Cycle(Strand, Minimums),
}

Mais une fois que j'ai ce type, alors autant faire de StrandResult<T> un alias :

type StrandResult<T> = Result<T, StrandFail>;

et c'est ce que j'ai fait.

Maintenant, cela ne semble pas nécessairement pire que d'avoir une seule énumération - mais cela semble un peu étrange. Habituellement, lorsque j'écris la documentation d'une fonction, par exemple, je ne "groupe" pas les résultats par "ok" et "erreur", mais parle plutôt des différentes possibilités comme "égales". Par exemple:

    /// Invoked when a strand represents an **answer**. This means
    /// that the strand has no subgoals left. There are two possibilities:
    ///
    /// - the strand may represent an answer we have already found; in
    ///   that case, we can return `StrandFail::NoSolution`, as this
    ///   strand led nowhere of interest.
    /// - the strand may represent a new answer, in which case it is
    ///   added to the table and `Ok` is returned.

Notez que je n'ai pas dit "nous retournons Err(StrandFail::NoSolution) . C'est parce que le Err ressemble à cet artefact ennuyeux que je dois ajouter.

(D'un autre côté, la définition actuelle aiderait les lecteurs à savoir quel est le comportement de ? sans consulter l'impl. Try )

Je suppose que ce résultat n'est pas si surprenant : l'implement Try actuel vous oblige à utiliser ? sur des choses qui sont isomorphes à Result . Cette uniformité n'est pas un accident, mais en conséquence, il est ennuyeux d'utiliser ? avec des choses qui ne sont pas fondamentalement "juste Result ". (D'ailleurs, c'est fondamentalement le même problème qui donne lieu à NoneError -- la nécessité de définir artificiellement un type pour représenter l'"échec" d'un Option .)

Je noterais également que, dans le cas de StrandFail , je ne veux pas particulièrement de la conversion From::from que les résultats ordinaires obtiennent, même si cela ne me fait pas de mal.

Le type Try::Error associé est-il déjà exposé à / utilisé par l'utilisateur directement ? Ou est-ce simplement nécessaire dans le cadre du désucrage de ? ? Dans ce dernier cas, je ne vois pas de problème réel avec juste définir « structurellement » - type Error = Option<Option<(Strand, Minimums)>> ou autre chose. (Devoir trouver l'équivalent structurel de la "moitié d'échec" de la définition de enum n'est pas génial, mais semble moins ennuyeux que de devoir reconfigurer l'ensemble de l'API publique.)

Je ne suis pas non plus. J'ai implémenté avec succès Try pour un type qui avait une représentation d'erreur interne et il me semblait naturel que Ok et Error soient du même type. Vous pouvez voir la mise en œuvre ici : https://github.com/kevincox/ecl/blob/8ca7ad2bc4775c5cfc8eb9f4309b2666e5163e02/src/lib.rs#L298 -L308

En fait, il semble qu'il existe un modèle assez simple pour faire ce travail.

impl std::ops::Try for Value {
    type Ok = Self;
    type Error = Self;

    fn from_ok(v: Self::Ok) -> Self { v }
    fn from_error(v: Self::Error) -> Self { v }
    fn into_result(self) -> Result<Self::Ok, Self::Error> {
        if self.is_ok() { Ok(val) } else { Err(val) }
    }
}

Si vous voulez déballer le succès, quelque chose comme ceci devrait fonctionner :

impl std::ops::Try for StrandFail<T> {
    type Ok = T;
    type Error = Self;

    fn from_ok(v: Self::Ok) -> Self { StrandFail::Success(v) }
    fn from_error(v: Self::Error) -> Self { v }
    fn into_result(self) -> Result<Self::Ok, Self::Error> {
        match self {
            StrandFail::Success(v) => Ok(v),
            other => Err(other),
        }
    }
}

Définir un type de structure est possible mais semble assez ennuyeux. J'accepte que je puisse utiliser Self . Cela me semble un peu farfelu que vous puissiez ensuite utiliser ? pour convertir de StrandResult en Result<_, StrandResult> etc.

Super rapport d'expérience, @nikomatsakis ! J'ai également été insatisfait de Try (de l'autre côté).

TL/DR : Je pense que FoldWhile compris, et nous devrions doubler l'interprétation Break -vs- Continue de ? au lieu de parler de en termes d'erreurs.

Plus long:

Je continue à utiliser Err pour quelque chose de plus proche du "succès" que de "l'erreur" parce que ? est si pratique.

Donc, si rien d'autre, je pense que la description que j'ai écrite pour Try est fausse et ne devrait pas parler d'une "dichotomie succès/échec", mais plutôt abstraite des "erreurs".

L'autre chose à laquelle j'ai pensé est que nous devrions envisager des impls légèrement fous pour Try . Par exemple, Ordering: Try<Ok = (), Error = GreaterOrLess> , combiné avec "try functions", pourrait permettre à cmp pour struct Foo<T, U> { a: T, b: U } d'être simplement

fn cmp(&self, other: &self) -> Ordering try {
    self.a.cmp(&other.a)?;
    self.b.cmp(&other.b)?;
}

Je ne sais pas encore si c'est le bon type de fou ou le mauvais type :rire: Il y a certainement une certaine élégance à cela, car une fois que les choses sont différentes, vous savez qu'elles sont différentes. Et essayer d'attribuer "succès" ou "erreur" de chaque côté ne semble pas bien convenir.

Je note également qu'il s'agit d'un autre cas où la "conversion d'erreurs" n'est pas utile. Je ne l'ai jamais utilisé non plus dans les exemples "Je veux ? mais ce n'est pas une erreur" ci-dessus. Et c'est certainement connu pour causer de la tristesse par inférence, alors je me demande si c'est une chose qui devrait être limitée au seul résultat (ou potentiellement supprimée en faveur de .map_err(Into::into) , mais c'est probablement infaisable).

Edit: Oh, et tout ce qui me fait me demander si la réponse à "Je continue à utiliser Result pour mes erreurs au lieu d'implémenter Try pour un type de mon propre" est "bon, c'est prévu".

Edit 2: Ce n'est pas différent de https://github.com/rust-lang/rust/issues/42327#issuecomment -318923393 ci-dessus

Edit 3: On dirait qu'une variante Continue a également été proposée dans https://github.com/rust-lang/rfcs/pull/1859#issuecomment -273985250

Mes deux centimes:

J'aime la suggestion de @fluffysquirrels de diviser le trait en deux traits. Un pour convertir en un résultat et un autre pour convertir à partir d'un résultat. Mais je pense que nous devrions garder le into_result ou l'équivalent dans le cadre du désucrage. Et je pense qu'à ce stade, nous devons le faire puisque l'utilisation de Option comme Try s'est stabilisée.

J'aime aussi l'idée de @scottmcm d'utiliser des noms qui suggèrent une pause/continue plutôt qu'une erreur/ok.

Pour mettre le code spécifique ici, j'aime la façon dont cela se lit :

https://github.com/rust-lang/rust/blob/ab8b961677ac5c74762dcea955aa0ff4d7fe4915/src/libcore/iter/iterator.rs#L1738 -L1746

Ce n'est bien sûr pas aussi agréable qu'une boucle droite, mais avec des "méthodes d'essai", ce serait proche :

self.try_for_each(move |x| try { 
    if predicate(&x) { return LoopState::Break(x) } 
}).break_value() 

A titre de comparaison, je trouve la version "vocabulaire des erreurs" vraiment trompeuse :

self.try_for_each(move |x| { 
    if predicate(&x) { Err(x) } 
    else { Ok(()) } 
}).err() 

Pouvons-nous implémenter Display pour NoneError ? Cela permettrait à la caisse d'échec de dériver automatiquement From<NoneError> for failure::Error . Voir https://github.com/rust-lang-nursery/failure/issues/61
Cela devrait être un changement de 3 lignes, mais je ne suis pas sûr du processus des RFC et autres.

@ cowang4 J'aimerais essayer d'éviter d'activer plus de mélange de résultat et d'option pour le moment, car le type est instable principalement pour garder nos options ouvertes là-bas. Je ne serais pas surpris si nous finissions par changer le design de Try et le desugar en quelque chose qui n'avait pas besoin de NoneError ...

@scottmcm D'accord. Je vois ce que tu veux dire. J'aimerais éventuellement un moyen propre de sucrer le modèle de renvoi d'Err lorsqu'une fonction de bibliothèque renvoie None. Peut-être en connaissez-vous un autre que Try ? Exemple:

fn work_with_optional_types(pb: &PathBuf) -> Result<MyStruct, Error> {
    if let Some(filestem) = pb.file_stem() {
        if let Some(filestr) = filestem.to_str() {
            return Ok(MyStruct {
                filename: filestr.to_string()
            });
        }
     }
    Err(_)
}

Une fois que j'ai trouvé cette fonctionnalité expérimentale et la caisse de failure , je me suis naturellement tourné vers :

use failure::Error;
fn work_with_optional_types(pb: &PathBuf) -> Result<MyStruct, Error> {
    Ok({
        title: pb.file_stem?.to_str()?.to_string()
    })
}

Ce qui fonctionne _presque_, à l'exception de l'absence d'un impl Display for NoneError comme je l'ai mentionné précédemment.
Mais, si ce n'est pas la syntaxe que nous aimerions utiliser, alors peut-être qu'il pourrait y avoir une autre fonction / macro qui simplifie le modèle :

if option.is_none() {
    return Err(_);
}

@cowang4 Je pense que cela fonctionnerait si vous implémentiez From<NoneError> pour failure::Error , qui utilisait votre propre type qui implémentait Display .

Cependant, il est probablement préférable d'utiliser opt.ok_or(_)? afin que vous puissiez dire explicitement quelle devrait être l'erreur si l'option est Aucune. Dans votre exemple, par exemple, vous voudrez peut-être une erreur différente si pb.file_stem est Aucun que si to_str() renvoie Aucun.

@tmccombs J'ai essayé de créer mon propre type d'erreur, mais je dois l'avoir mal fait. C'était comme ça :

#[macro_use] extern crate failure_derive;

#[derive(Fail, Debug)]
#[fail(display = "An error occurred.")]
struct SiteError;

impl From<std::option::NoneError> for SiteError {
    fn from(_err: std::option::NoneError) -> Self {
        SiteError
    }
}

fn build_piece(cur_dir: &PathBuf, piece: &PathBuf) -> Result<Piece, SiteError> {
    let title: String = piece
        .file_stem()?
        .to_str()?
        .to_string();
    Ok(Piece {
        title: title,
        url: piece
            .strip_prefix(cur_dir)?
            .to_str()
            .ok_or(err_msg("tostr"))?
            .to_string(),
    })
}

Et puis j'ai essayé d'utiliser mon type d'erreur...

error[E0277]: the trait bound `SiteError: std::convert::From<std::path::StripPrefixError>` is not satisfied
   --> src/main.rs:195:14
    |
195 |           url: piece
    |  ______________^
196 | |             .strip_prefix(cur_dir)?
    | |___________________________________^ the trait `std::convert::From<std::path::StripPrefixError>` is not implemented for `SiteError`
    |
    = help: the following implementations were found:
              <SiteError as std::convert::From<std::option::NoneError>>
    = note: required by `std::convert::From::from`

error[E0277]: the trait bound `SiteError: std::convert::From<failure::Error>` is not satisfied
   --> src/main.rs:195:14
    |
195 |           url: piece
    |  ______________^
196 | |             .strip_prefix(cur_dir)?
197 | |             .to_str()
198 | |             .ok_or(err_msg("tostr"))?
    | |_____________________________________^ the trait `std::convert::From<failure::Error>` is not implemented for `SiteError`
    |
    = help: the following implementations were found:
              <SiteError as std::convert::From<std::option::NoneError>>
    = note: required by `std::convert::From::from`

D'accord, je viens de réaliser qu'il veut savoir comment convertir d'autres types d'erreurs en mon erreur, que je peux écrire de manière générique :

impl<E: failure::Fail> From<E> for SiteError {
    fn from(_err: E) -> Self {
        SiteError
    }
}

Nan...

error[E0119]: conflicting implementations of trait `std::convert::From<SiteError>` for type `SiteError`:
   --> src/main.rs:183:1
    |
183 | impl<E: failure::Fail> From<E> for SiteError {
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |
    = note: conflicting implementation in crate `core`:
            - impl<T> std::convert::From<T> for T;

D'accord, qu'en est-il de std::error::Error ?

impl<E: std::error::Error> From<E> for SiteError {
    fn from(_err: E) -> Self {
        SiteError
    }
}

Cela ne fonctionne pas non plus. En partie parce que cela entre en conflit avec mon From<NoneError>

error[E0119]: conflicting implementations of trait `std::convert::From<std::option::NoneError>` for type `SiteError`:
   --> src/main.rs:181:1
    |
175 | impl From<std::option::NoneError> for SiteError {
    | ----------------------------------------------- first implementation here
...
181 | impl<E: std::error::Error> From<E> for SiteError {
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `SiteError`
    |
    = note: upstream crates may add new impl of trait `std::error::Error` for type `std::option::NoneError` in future versions

Ce qui est bizarre parce que je pensais que NoneError n'avait pas implémenté std::error::Error . Lorsque je commente mes impl From<NoneError> non génériques, j'obtiens :

error[E0277]: the trait bound `std::option::NoneError: std::error::Error` is not satisfied
   --> src/main.rs:189:25
    |
189 |       let title: String = piece
    |  _________________________^
190 | |         .file_stem()?
    | |_____________________^ the trait `std::error::Error` is not implemented for `std::option::NoneError`
    |
    = note: required because of the requirements on the impl of `std::convert::From<std::option::NoneError>` for `SiteError`
    = note: required by `std::convert::From::from`

Dois-je écrire tous les From s manuellement. Je pensais que la caisse d'échec était censée les dériver?

Peut-être que je devrais m'en tenir à option.ok_or()

Dois-je écrire tous les Froms manuellement. Je pensais que la caisse d'échec était censée les dériver?

Je ne pense pas que la caisse d'échec fasse ça. Mais je peux me tromper.

Ok, j'ai donc réexaminé la caisse d'échec, et si je lis bien la documentation et les différentes versions, elle est conçue pour toujours utiliser le failure::Error comme type d'erreur dans vos Result s, voir ici . Et, il implémente un trait de couverture impl Fail pour la plupart des types d'erreur :

impl<E: StdError + Send + Sync + 'static> Fail for E {}

https://github.com/rust-lang-nursery/failure/blob/master/failure-1.X/src/lib.rs#L218

Et puis un impl From pour qu'il puisse Try / ? autres erreurs (comme celles de std) dans le type global failure::Error .
rust impl<F: Fail> From<F> for ErrorImpl
https://github.com/rust-lang-nursery/failure/blob/d60e750fa0165e9c5779454f47a6ce5b3aa426a3/failure-1.X/src/error/error_impl.rs#L16

Mais, c'est juste que puisque l'erreur relative à ce problème de rouille, NoneError , est expérimentale, elle ne peut pas encore être convertie automatiquement, car elle n'implémente pas le trait Display . Et nous ne le voulons pas, car cela brouille la frontière entre Option s et Result s de plus. Tout sera probablement retravaillé et réglé éventuellement, mais pour l'instant, je m'en tiendrai aux techniques sans sucre que j'ai apprises.

Merci à tous pour votre aide. J'apprends lentement Rust ! :le sourire:

Je vais m'en tenir aux techniques sans sucre que j'ai apprises.

:+1: Honnêtement, je pense que .ok_or(...)? restera la voie à suivre (ou .ok_or_else(|| ...)? , bien sûr). Même si NoneError _avait_ un Display impl, que dirait-il ? « Quelque chose n'était pas là » ? Ce n'est pas une grande erreur...

Tenter de se rapprocher d'une proposition concrète...

Je commence à aimer l'alternative TrySuccess . Fait intéressant, je pense que _personne_, y compris moi-même, n'aimait celui-là à l'origine - ce n'est même pas dans la version finale de la RFC. Mais heureusement, cela perdure dans l'histoire : https://github.com/rust-lang/rfcs/blob/f89568b1fe5db4d01c4668e0d334d4a5abb023d8/text/0000-try-trait.md#using -an-associated-type-for-the-success -valeur

Il me semble que la plus grande objection à cela était la plainte raisonnable qu'un trait de base entier pour juste un type associé ( trait TrySuccess { type Success; } ) était exagéré. Mais avec catch try blocs de retour (https://github.com/rust-lang/rust/issues/41414#issuecomment-373985777) pour faire ok-wrapping (comme dans le RFC) , tout d'un coup il a un usage direct et important : c'est le trait qui contrôle cet emballage. Couplé à l'objectif " ? devrait toujours produire le même type" que je pense était généralement souhaité, le trait semble mieux tenir son poids. Homme de paille:

trait TryContinue {
    type Continue;
    fn from_continue(_: Self::Continue) -> Self;
}

Ce type associé, comme celui qui sera renvoyé par l'opérateur ? , est également celui qui doit clairement exister. Cela signifie que cela ne touche pas le désagrément « devait définir une sorte de type « résiduel » » que Niko a articulé . Et cela étant () est raisonnable, voire commun, évite donc les contorsions de type NoneError .

Edit: Pour le bikeshed, "retour" pourrait être un bon mot, car c'est ce qui se passe lorsque vous return partir d'une méthode try (aka ok-wrapping). return est également le nom de l'opérateur monade pour cela, iiuc...

Edit 2: Mes réflexions sur les autres traits/méthodes ne se sont pas encore stabilisées, @tmccombs.

@scottmcm juste pour être clair, ou vous suggérez les deux traits suivants ?

trait TryContinue {
  type Continue;
  fn from_continue(_: Self::Continue) -> Self;
}

trait Try<E>: TryContinue {
  fn try(self) -> Result<Self::Continue, E>
}

Et désgurer x? ressemblerait à quelque chose comme :

x.try() match {
    Ok(c) => c,
   Err(e) => throw e // throw here is just a placeholder for either returning or breaking out of a try block
}

et try { ...; expr} désucraient à quelque chose comme :

{ 
    ...
    TryContinue::from_continue(expr);
}

@scottmcm, moi aussi, je trouve cette variante beaucoup plus attrayante lorsque l'on considère ok-wrapping =)

En poursuivant avec le trait suivant, je pense que celui pour ? devient celui-ci (modulo massive bikeshedding) :

trait Try<Other: TryContinue = Self>: TryContinue + Sized {
    fn check(x: Other) -> ControlFlow<Other::Continue, Self>;
}

enum ControlFlow<C, B> {
    Continue(C),
    Break(B),
}

Justificatifs assortis :

  • Cela nécessite TryContinue pour son type d'argument afin que le type d'une expression x? soit toujours le même
  • Cela définit par défaut le paramètre de type sur Self pour des cas simples comme try_fold qui inspectent et renvoient le même type, donc ça va avec seulement T: Try .
  • Ce trait étend TryContinue car si un type utilisé comme type de retour autorise ? dans son corps, alors il devrait également prendre en charge l'ok-wrapping.
  • La nouvelle énumération est isomorphe au résultat, mais évite de parler en termes d'"erreurs" comme indiqué ci-dessus et, par conséquent, je pense qu'elle fournit une signification très claire pour chacune des variantes.
  • L'argument de ? est le type générique de sorte que, comme TryContinue , il "produise un Self partir de quelque chose"

Démo de preuve de concept complète, y compris les macros pour try{} / ? et les impls pour Option , Result , et Ordering : https ://play.rust-lang.org/?gist=18663b73b6f35870d20fd172643a4f96&version=stable (merci @nikomatsakis d' avoir l'original il y a un an 🙂)

Inconvénients :

  • A toujours les paramètres de type fantôme dans l' Result que je n'aime pas vraiment

    • Mais c'est peut-être un bon compromis pour ne pas avoir besoin d'un type LessOrGreater supplémentaire dans le Ordering impl.

    • Et les paramètres de type fantôme dans les impls sont moins dégoûtants que dans les types de toute façon

  • Ne donne pas une transformation From cohérente

    • Mais cela pourrait être bien, car StrandResult s'en fichait de toute façon, pour quelque chose comme SearchResult ce serait bizarre car le chemin Break est le chemin du succès, et cela pourrait finir par aider à l'inférence dans les cas imbriqués ? toute façon.

  • La syntaxe de throw n'est pas aussi évidente

    • Ce qui perd l'explication de ? comme Ok(x) => x, Err(r) => throw e.into() que j'ai vraiment aimé

    • Mais pourrait également laisser throw; être le moyen de produire None (via quelque chose comme impl<T> Throw<()> for Option<T> ), ce qui est bien meilleur que throw NoneError;

    • Et séparer cela pourrait être bon de toute façon, puisque throw LessOrGreater::Less aurait été _vraiment_ idiot.

Edit : Ping @glaebhoerl , dont j'aimerais avoir l'opinion à ce sujet en tant que grand participant à la RFC 1859.

Edit 2 : Aussi cc @colin-kiegel, pour cette déclaration de https://github.com/rust-lang/rfcs/pull/1859#issuecomment -287402652 :

Je me demande si l'approche essentialiste pourrait adopter une partie de l'élégance des réductionnistes sans sacrifier les objectifs ci-dessus.

J'aime beaucoup cette proposition.

La syntaxe de lancer n'est pas aussi évidente

Ignorant le bagage du mot-clé throw des autres langages, "throw" a en quelque sorte un sens, dans la mesure où vous "lancez" la valeur vers une portée plus élevée. Python et scala utilisent également des exceptions pour le flux de contrôle autres que les cas d'erreur (bien que, dans le cas de scala, vous n'utiliseriez généralement pas directement try/catch), il existe donc un précédent pour l'utilisation de throw pour des chemins réussis.

@tmccombs

Ignorant le bagage du mot-clé throw des autres langages, "throw" a en quelque sorte un sens, dans la mesure où vous "lancez" la valeur vers une portée plus élevée.

Bien qu'il soit livré avec des bagages similaires, je soupçonne que « augmenter » convient mieux :

lancer (v) : mettre ou faire aller ou venir dans un endroit, une position, une condition, etc., comme en lançant :

augmenter (v) : passer à une position plus élevée ; relever; élever

Il existe peut-être un moyen de combiner « relancer » avec la logique de ? , étant donné que relancer peut également signifier « collecter ». Quelque chose comme : Ok(v) => v, Err(e) => raise From::from(e) , où raise imite le motif correspondant (par exemple, étant donné un motif Err(e) c'est de la magie syntaxique pour ControlFlow::Break(Err(...)) ).

Je soupçonne que "augmenter" est un meilleur ajustement

bon point

@scottmcm

La syntaxe de lancer n'est pas aussi évidente

Y a-t-il une raison pour laquelle nous ne pouvons pas avoir from_err(value: Other) ?

MISE À JOUR : Je suis peut-être confus au sujet du rôle de Other , en fait. Je dois étudier ce trait un peu plus. =)

@nikomatsakis

Y a-t-il une raison pour laquelle nous ne pouvons pas avoir from_err(value: Other) ?

Eh bien, Other est un type complet de ? (comme un Result ), donc je ne voudrais pas que throw Ok(4) fonctionne. (Et cela doit être toute la disjonction pour éviter de forcer l'introduction d'un type d'erreur artificiel.) Par exemple, je pense que notre interopérabilité Option-Résultat actuelle serait équivalente à ceci (et l'inverse):

impl<T, F, U> Try<Option<U>> for Result<T, F>
   where F: From<NoneError>
{
    fn check(x: Option<U>) -> ControlFlow<U, Self> {
        match x {
            Some(x) => ControlFlow::Continue(x),
            None => ControlFlow::Break(Err(From::from(NoneError))),
        }
    }
}

Mon inclination actuelle pour throw serait pour ceci :

trait TryBreak<T> : TryContinue { from_break(_: T) -> Self }

avec

throw $expr  ==>  break 'closest_catch TryBreak::from_break($expr)
  • Étend TryContinue afin que ok-wrapping soit également disponible pour le type de retour de la méthode dans laquelle il est utilisé
  • Pas un type associé, vous pouvez donc implémenter les deux, disons, TryBreak<TryFromIntError> et TryBreak<io::Error> pour un type si vous le souhaitez.

    • et impl<T> TryBreak<()> for Option<T> pour activer seulement throw; semble plausible d'une manière qu'un type d'erreur associé de () ne l'a pas fait.

    • impl<T> TryBreak<!> for T serait bien aussi, mais c'est probablement incohérent.

(A part : ces noms de traits et de méthodes sont terribles ; aidez-nous s'il vous plaît !)

Mes réflexions sur les autres problèmes soulevés ici ne se sont pas encore concrétisées dans une forme facilement articulable, mais en ce qui concerne les problèmes d'inférence de type autour du désucrage de From::from() (je ne me souviens pas si cela a également été discuté ailleurs récemment , ou seulement ici ?) :

Le sens instinctif (d'après l'expérience de Haskell) que "de cette façon, les ambiguïtés d'inférence de type mensonge" étaient l'une des raisons (mais pas les principales) pour lesquelles je ne voulais pas avoir une conversion From dans le cadre de la RFC originale. Maintenant qu'il est cuit dans le gâteau, je me demande si nous ne pourrions pas essayer d'avoir ce gâteau et de le manger aussi en « juste » en ajoutant une règle spéciale par défaut pour cela au processus d'inférence de type ?

C'est-à-dire, je pense : chaque fois que nous voyons un From::from() [éventuellement : un qui a été inséré par le désucrage de ? , plutôt qu'écrit manuellement], et nous connaissons exactement l'un des deux types (entrée par rapport à la sortie), tandis que l'autre est ambigu, nous par défaut l'autre est le même que l'un. En d'autres termes, je pense que, lorsqu'il n'est pas clair quel impl From utiliser, nous utilisons par défaut impl<T> From<T> for T . C'est, je pense, fondamentalement toujours ce que vous voulez réellement ? C'est peut-être un peu ad-hoc, mais si cela fonctionne, les avantages semblent bien valoir les coûts à mon humble avis.

(Je pensais aussi que From était déjà un élément lang, précisément à cause du désucrage de ? , mais il ne semble pas l'être ? En tout cas, c'est déjà à certains égards spécial pour cette raison .)

dans la proposition de @scottmcm , From::from() fait _pas_ partie du désucrage, mais plutôt de l'implémentation de Try pour Result .

@tmccombs Je ne proposais pas d'amendement à sa proposition.

Le try{} RFC (https://github.com/rust-lang/rfcs/pull/2388) a discuté des blocs try où l'on ne se soucie pas du résultat. Cette alternative semble gérer cela décemment, car elle peut choisir d'ignorer complètement les types d'erreur si elle le souhaite, permettant

let IgnoreErrors = try {
    error()?;
    none()?;
};

Implémentation de la preuve de concept utilisant les mêmes traits que précédemment : https://play.rust-lang.org/?gist=e0f6677632e0a9941ed1a67ca9ae9c98&version=stable

Je pense qu'il y a là des possibilités intéressantes, d'autant plus qu'une implémentation personnalisée pourrait, disons, uniquement prendre des résultats et lier E: Debug afin qu'elle enregistre automatiquement toute erreur qui se produit. Ou une version pourrait être conçue spécifiquement comme un type de retour pour main en conjonction avec Termination qui "fonctionne simplement" pour vous permettre d'utiliser ? sans signature de type complexe (https ://github.com/rust-lang/rfcs/issues/2367).

J'ai eu des problèmes similaires à ceux mis en évidence par @nikomatsakis en utilisant la version existante du trait Try . Pour les problèmes spécifiques, voir https://github.com/SergioBenitez/Rocket/issues/597#issuecomment -381533108.

Les définitions de traits proposées par @scottmcm résolvent ces problèmes. Ils semblent cependant plus compliqués que nécessaire. J'ai essayé de les réimplémenter et j'ai trouvé ce qui suit :

#[derive(Debug, Copy, Clone)]
enum ControlFlow<C, B> {
    Continue(C),
    Break(B),
}

// Used by `try { }` expansions.
trait FromTry: Try {
    fn from_try(value: Self::Continue) -> Self;
}

// Used by `?`.
trait Try<T = Self>: Sized {
    type Continue;
    fn check(x: T) -> ControlFlow<Self::Continue, Self>;
}

Le principal changement est que le type associé Continue est sur le trait Try par opposition au FromTry (auparavant TryContinue ). En plus de simplifier les définitions, cela a l'avantage que Try peut être implémenté indépendamment de FromTry , que je postule comme étant plus courant, et que l'implémentation de FromTry est simplifiée une fois Try est implémenté. (Remarque : si l'on souhaite que Try et FromTry soient implémentés à l'unisson, nous pouvons simplement déplacer la méthode from_try dans Try )

Voir le terrain de jeu complet avec des implémentations pour Result et Option ainsi que le Outcome de Rocket sur ce terrain de jeu .

Merci pour le rapport, @SergioBenitez ! Cette implémentation correspond à la version alternative "retourner les paramètres de type" de la proposition de trait d'origine dans la RFC 1859 : https://github.com/rust-lang/rfcs/blob/f89568b1fe5db4d01c4668e0d334d4a5abb023d8/text/0000-try-trait.md#unresolved -des questions

La plus grande chose qui perd est la propriété que typeof(x?) ne dépend que de typeof(x) . L'absence de cette propriété était l'une des préoccupations de l'original ("Je m'inquiète un peu de la lisibilité du code le long de ces lignes" https://github.com/rust-lang/rfcs/pull/1859#issuecomment-279187967) et un avantage de la proposition réductionniste finale ("Pour tout type T donné, ? peut produire exactement un type de valeur ok/erreur" https://github.com/rust-lang/rfcs/pull/1859#issuecomment-283104310). Bien sûr, il y avait aussi des arguments selon lesquels cette propriété est inutile ou trop restrictive, mais dans le résumé final avant FCP, elle était toujours là comme un avantage (https://github.com/rust-lang/rfcs/pull/1859#issuecomment -295878466).

En plus de simplifier les définitions, cela a l'avantage que Try peut être implémenté indépendamment de FromTry , ce que je pose comme plus courant

Certes, aujourd'hui, from_ok est moins courant, car Try et do catch sont instables. Et même si do catch étaient stables, je suis d'accord qu'il sera utilisé moins de ? ensemble (puisque la plupart de ces blocs contiennent plusieurs ? s).

Du point de vue du trait et de ses opérations, cependant, je pense que « envelopper une valeur « continuer » dans le type de support" est une partie absolument essentielle de ? . On passe rarement en revue le trait pour cela aujourd'hui - en utilisant simplement Ok(...) ou Some(...) place - mais c'est essentiel pour une utilisation générique. Par exemple, try_fold :

https://github.com/rust-lang/rust/blob/8728c7a726f3e8854f5a80b474d1a8bacab10304/src/libcore/iter/iterator.rs#L1478 -L1482

Si une fonction doit renvoyer un type de support, il est essentiel qu'il existe un moyen de mettre la valeur d'intérêt dans ce type de support. Et, surtout, je ne pense pas que fournir cela soit une épreuve. Il a une définition très naturelle pour le résultat, pour l'option, pour le résultat, etc.

@Centril a également souligné auparavant que "envelopper un scalaire dans un support" est également une construction plus simple en théorie. Par exemple, Haskell l'appelle Pointed typeclass , bien que je ne pense pas que nous voulions le généraliser _aussi loin dans Rust : autoriser try { 4 }vec![4] me semble exagéré .

J'imagine également un futur où, comme les fonctions async sont proposées pour encapsuler la valeur du bloc dans un futur, nous pourrions avoir des fonctions try qui encapsulent la valeur du bloc dans un type faillible. Là encore, le TryContinue est la partie critique -- une telle fonction pourrait même ne pas utiliser ? , si nous avons une construction throw .

Donc tout cela est malheureusement plus philosophique que concret. Faites-moi savoir si cela avait un sens, ou si vous pensez le contraire dans l'une des parties :slightly_smiliing_face:

Edit : Désolé si vous avez reçu un e-mail avec une première version de ceci ; J'ai appuyé sur "commenter" trop tôt...

La plus grande chose qui perd est la propriété que typeof(x?) ne dépend que de typeof(x).

Ah oui, absolument. Merci d'avoir fait remarquer cela.

Bien sûr, il y avait aussi des arguments selon lesquels cette propriété est inutile ou trop restrictive, mais dans le résumé final avant FCP, elle était toujours là comme un avantage (rust-lang/rfcs#1859 (commentaire)).

Y a-t-il des exemples spécifiques de cas où cela pourrait être trop restrictif ?

Si une fonction doit renvoyer un type de support, il est essentiel qu'il existe un moyen de mettre la valeur d'intérêt dans ce type de support. Et, surtout, je ne pense pas que fournir cela soit une épreuve.

Je pense que c'est une analyse juste. Je suis d'accord.

@SergioBenitez De https://github.com/rust-lang/rfcs/pull/1859#issuecomment-279187967

La question est donc de savoir si les restrictions proposées sont suffisantes ? Existe-t-il de bonnes utilisations du contexte de cas d'erreur pour déterminer le type de réussite ? Y a-t-il des abus probables ?

Je peux dire d'après mon expérience dans les futurs qu'il pourrait bien y avoir des cas utiles ici. En particulier, le type de sondage dont vous parlez peut être traité de plusieurs manières. Parfois, nous voulons sauter sur la variante NotReady et nous retrouver avec essentiellement une valeur de résultat. Parfois, nous ne sommes intéressés que par la variante Ready et voulons sauter sur l'une des autres variantes (comme dans votre croquis). Si nous permettons au type de réussite d'être déterminé en partie par le type d'erreur, il est plus plausible de prendre en charge ces deux cas et d'utiliser essentiellement le contexte pour déterminer le type de décomposition souhaité.

OTOH, je m'inquiète un peu de la lisibilité du code dans ce sens. Cela semble qualitativement différent de la simple conversion de la composante d'erreur - cela signifie que la base correspond à cela ? serait performant dépend des informations contextuelles.

On pourrait donc imaginer un trait qui déplace les _deux_ types vers des paramètres de type, comme

trait Try<T,E> {
    fn question(self) -> Either<T, E>;
}

Et utilisez-le pour activer tous

let x: Result<_,_> = blah.poll()?; // early-return if NotReady
let x: Async<_> = blah.poll()?; // early-return if Error
let x: i32 = blah.poll()?; // early-return both NotReady and Errors

Mais je pense que c'est définitivement une mauvaise idée, car cela signifie que ceux-ci ne fonctionnent pas

println!("{}", z?);
z?.method();

Puisqu'il n'y a pas de contexte de type pour dire quoi produire.

L'autre version serait d'activer des choses comme ceci:

fn foo() -> Result<(), Error> {
    // `x` is an Async<i32> because NotReady doesn't fit in Result
    let x = something_that_returns_poll()?;
}
fn bar() -> Poll<(), Error> {
    // `x` is just i32 because we're in a Poll-returning method
    let x = something_that_returns_poll()?;
}

Mon instinct, c'est que l'inférence « s'écoule du ? il est trop surprenant, et donc c'est dans le seau "trop ​​intelligent".

De manière critique, je ne pense pas que ne pas l'avoir soit trop restrictif. my_result? dans une fonction -> Poll n'en a pas besoin, car le type de réussite est le même que d'habitude (important pour que le code synchrone fonctionne de la même manière dans des contextes asynchrones) et la variante d'erreur se convertit bien aussi . Utiliser ? sur un Poll dans une méthode qui renvoie Result semble de toute façon être un anti-modèle, pas quelque chose qui devrait être commun, donc utiliser des méthodes dédiées (hypothétiques) comme .wait(): T (à bloquer pour le résultat) ou .ready(): Option<T> (pour vérifier si c'est fait) pour choisir le mode est probablement mieux de toute façon.

C'est "intéressant" https://play.rust-lang.org/?gist=d3f2cd403981a631f30eba2c3166c1f4&version=nightly&mode=debug

Je n'aime pas ces blocs try (do catch), ils ne semblent pas très conviviaux pour les nouveaux arrivants.

J'essaie de rassembler l'état actuel de la proposition, qui semble réparti sur plusieurs commentaires. Existe-t-il un seul résumé de l'ensemble de traits actuellement proposé (qui supprime le type associé Error ) ?

Au début de ce fil, je vois un commentaire sur la division de Try en traits d'erreur distincts vers/depuis - y a-t-il des plans pour implémenter cette division ?

Il serait utile d'avoir une conversion transparente de Result<T, E> vers n'importe quel type Other<T2, E> sur un point d'interrogation -- cela permettrait aux fonctions IO existantes d'être appelées avec une syntaxe agréable à partir d'une fonction avec un plus spécialisé (par exemple paresseux) type de retour.

pub fn async_handler() -> AsyncResult<()> {
    let mut file = File::create("foo.txt")?;
    AsyncResult::lazy(move || {
        file.write_all(b"Hello, world!")?;
        AsyncResult::Ok(())
    })
}

Sémantiquement, cela ressemble à From::from(E) -> Other<T2, E> , mais l'utilisation de From est actuellement limitée à l'implémentation Result -équivalente Try existante.

Je pense vraiment que NoneError devrait avoir un problème de suivi séparé. Même si le trait Try ne se stabilise jamais, NoneError devrait se stabiliser car cela rend l'utilisation de ? sur Option beaucoup plus ergonomique. Considérez ceci pour les erreurs telles que struct MyCustomSemanthicalError; ou les erreurs d'implémentation de Default . None pourrait facilement être converti en MyCustomSeemanthicalError via From<NoneError> .

En travaillant sur https://github.com/rust-analyzer/rust-analyzer/ , j'ai rencontré un papercut légèrement différent des insuffisances de l'opérateur ? , en particulier lorsque le type de retour est Result<Option<T>, E> .

Pour ce type, il est logique que ? déshydrate efficacement pour :

match value? {
    None => return Ok(None),
    Some(it)=>it,
}

value est de type Result<Option<V>, E> , ou :

value?

value est Result<V, E> . Je pense que cela est possible si les bibliothèques standard implémentent Try de la manière suivante pour Option<T> , bien que je n'aie pas explicitement testé cela et je pense qu'il peut y avoir des problèmes de spécialisation.

enum NoneError<E> {
    None,
    Err(E),
}

impl From<T> for NoneError<T> {
    fn from(v: T) -> NoneError<T> {
        NoneError:Err(v)
    }
}

impl<T, E> std::Ops::Try for Result<Option<T>, E> {
    type OK = T;
    type Error = NoneError<E>;
    fn into_result(self) -> Result<Self::OK, Self::Error> {
        match self {
            Ok(option) => {
                if let Some(inner) = option {
                    Ok(inner)
                } else {
                    Err(NoneError::None)
                }
            }
            Err(error) => {
                Err(NoneError::Err(error));
            }
        }
    }
    fn from_error(v: Self::Error) -> Self {
        match v {
            NoneError::Err(error) => Err(error),
            None => Some(None),
        }
    }
    fn from_ok(v: Self::OK) -> Self {
        Ok(Some(v))
    }
}

impl<T> std::Ops::Try for Option<T> {
    type OK = T;
    type Error = NoneError<!>;
    fn into_result(self) -> Result<Self::OK, Self::Error> {
        match self {
            None => Err(NoneError::None),
            Some(v) => Ok(v),
        }
    }
    fn from_error(v: Self::Error) -> Self {
        match v {
            NoneError::None => Some(None),
            _ => unreachable!("Value of type ! obtained"),
        }
    }
    fn from_ok(v: Self::OK) -> Self {
        Ok(v)
    }
}

Lorsqu'il a demandé à @matklad pourquoi il ne pouvait pas créer une énumération personnalisée implémentant Try , qui s'appellerait Cancellable dans ce cas, il a souligné que std::ops::Try est instable, donc il ne peut pas être utilisé de toute façon étant donné que rust-analyzer (actuellement) cible la rouille stable.

Republiez depuis https://github.com/rust-lang/rust/issues/31436#issuecomment -441408288 parce que je voulais commenter cela, mais je pense que ce n'était pas le bon problème :

Essentiellement, une situation que j'ai est des rappels dans un framework GUI - au lieu de retourner un Option ou Result , ils doivent retourner un UpdateScreen , pour dire au framework si l'écran doit être mis à jour ou non. Souvent, je n'ai pas du tout besoin de me connecter (il n'est tout simplement pas pratique de me connecter à chaque erreur mineure) et je renvoie simplement un UpdateScreen::DontRedraw lorsqu'une erreur s'est produite. Cependant, avec l'opérateur ? actuel, je dois écrire ceci tout le temps :

let thing = match fs::read(path) {
    Ok(o) => o,
    Err(_) => return UpdateScreen::DontRedraw,
};

Comme je ne peux pas convertir un Result::Err en UpdateScreen::DontRedraw via l'opérateur Try, cela devient très fastidieux - j'ai souvent de simples recherches dans les cartes de hachage qui peuvent échouer (ce qui n'est pas une erreur ) - si souvent dans un rappel, j'ai 5 à 10 utilisations de l'opérateur ? . Parce que ce qui précède est très détaillé à écrire, ma solution actuelle est de impl From<Result<T>> for UpdateScreen comme ceci , puis d'utiliser une fonction interne dans le rappel comme ceci :

fn callback(data: &mut State) -> UpdateScreen {
     fn callback_inner(data: &mut State) -> Option<()> {
         let file_contents = fs::read_to_string(data.path).ok()?;
         data.loaded_file = Some(file_contents);
         Some(())
     }

    callback_inner(data).into()
}

Étant donné que le rappel est utilisé comme pointeur de fonction, je ne peux pas utiliser un -> impl Into<UpdateScreen> (pour une raison quelconque, renvoyer un impl n'est actuellement pas autorisé pour les pointeurs de fonction). Donc, la seule façon pour moi d'utiliser l'opérateur Try est de faire l'astuce de la fonction interne. Ce serait bien si je pouvais simplement faire quelque chose comme ça :

impl<T> Try<Result<T>> for UpdateScreen {
    fn try(original: Result<T>) -> Try<T, UpdateScreen> {
        match original {
             Ok(o) => Try::DontReturn(o),
             Err(_) => Try::Return(UpdateScreen::DontRedraw),
        }
    }
}

fn callback(data: &mut State) -> UpdateScreen {
     // On any Result::Err, convert to an UpdateScreeen::DontRedraw and return
     let file_contents = fs::read_to_string(data.path)?;
     data.loaded_file = Some(file_contents);
     UpdateScreen::Redraw
}

Je ne sais pas si cela serait possible avec la proposition actuelle et je voulais juste ajouter mon cas d'utilisation pour examen. Ce serait formidable si un opérateur Try personnalisé pouvait prendre en charge quelque chose comme ça.

@joshtriplett Désolé d'avoir mis du temps à revenir là- https://github.com/rust-lang/rust/compare/master...scottmcm :try-trait-v2 pour être concret. J'espère essayer d'autres choses avec.

@scottmcm Avez-vous une explication de niveau supérieur sur les changements ?

@scottmcm FWIW J'ai également essayé vos modifications en rayonne :
https://github.com/rayon-rs/rayon/compare/master...cuviper :try-trait-v2
(en utilisant toujours des copies privées plutôt que des éléments instables)

Alors, quelle est la solution pour convertir l'option NoneError ? Il semble que, parce qu'il implémente le trait Try, il ne sera pas compilé à moins que vous ne l'activiez à l'aide de cette fonctionnalité particulière (instable ?).

Donc, en gros, vous ne pouvez pas utiliser le ? opérateur avec Option pour autant que je sache ?

@omarabid , l'opérateur est stable pour une utilisation avec Option ou Result , mais vous ne pouvez pas utiliser Try comme contrainte générique tant qu'il n'est pas stable. C'est parfaitement bien d'utiliser ? sur un Option dans une fonction renvoyant Option , car vous n'avez pas du tout à impliquer NoneError . Vous pouvez également renvoyer un Result si vous effacez les types :

use std::fmt::Debug;

pub struct Error(Box<dyn Debug>);

impl<T: Debug + 'static> From<T> for Error {
    fn from(error: T) -> Self {
        Error(Box::new(error))
    }
}

pub fn try_get<T>(slice: &[T], index: usize) -> Result<&T, Error> {
    Ok(slice.get(index)?)
}

( aire de jeux )

@scottmcm , votre prototype try-trait-v2 échoue cet exemple !

Si nous ne voulons pas que mon exemple se brise, try-trait-v2 aura besoin de quelque chose comme :

#[unstable(feature = "try_trait_v2", issue = "42327")]
impl<T, U, E: From<NoneError>> ops::Bubble<Result<U, E>> for Option<T> {
    fn bubble(self) -> ops::ControlFlow<T, Result<U, E>> {
        match self {
            Some(x) => ops::ControlFlow::Continue(x),
            None => ops::ControlFlow::Break(Err(E::from(NoneError))),
        }
    }
}

Quel est l'état actuel de cette fonctionnalité ?

Le PR #62606 pour documenter l'implémentation de try_fold pour les itérateurs devrait être rouvert une fois que cela deviendra stable.

Edit : mise à jour de l'opération avec un élément de suivi pour cela ~ scottmcm

Existe-t-il des plans pour remplacer le trait Try par l'une des alternatives suggérées dans ce fil ? La version suggérée par @scottmcm semble bien. Je veux continuer à utiliser l'opérateur ? avec Option , et je pense que le trait Try devrait être modifié pour ne pas forcer la sémantique Result sur Option .

L' utilisation de l'alternative de ? avec Option et de nous débarrasser de NoneError . Je suis d'accord avec @nikomatsakis ( commentaire ) que le trait Try ne devrait pas promouvoir la nécessité de "définir artificiellement un type pour représenter l'échec d'un Option ".

pub struct Error(Box<dyn std::fmt::Debug>);
impl<T: std::fmt::Debug + 'static> From<T> for Error { fn from(error : T) -> Self { Error(Box::new(error)) } }
type Result<T> = std::result::Result<T, Error>;

Débutant ici, j'étais un peu têtu à vouloir obtenir ? pour taper automatiquement effacer à la fois toute erreur et option.
Après avoir passé beaucoup trop de temps à essayer de comprendre pourquoi d'autres solutions probables ne peuvent pas être mises en œuvre, j'ai trouvé que @cuviper est le plus proche de ce que je peux obtenir.
Quelques explications auraient aidé, mais au moins j'ai pu me familiariser de près avec certaines limitations de la métaprogrammation Rust.
J'ai donc essayé de comprendre et d'expliquer en termes concrets.
Ce fil semble le carrefour le plus probable où je peux, espérons-le, aider tous ceux comme moi qui trébuchent là-dessus, n'hésitez pas à corriger :

  • Utilisation de Debug (commun à StdError et NoneError) au lieu de StdError :
    Un From<T: StdError> for Error générique et un From<NoneError> for Error spécialisé conflits
    Parce qu'il serait ambigu si NoneError implémentait StdError (apparemment, même la valeur par défaut n'autorise pas les remplacements et applique l'exclusivité)
    Cela pourrait être résolu par une "limite négative" qui n'est pas prise en charge, peut-être parce que ce serait le seul cas d'utilisation ? (sur spécialisation)
    impl StdError pour NoneError ne peut être défini qu'avec StdError ou NoneError lui-même, de sorte qu'il soit cohérent dans tout en aval.
  • L'erreur ne peut pas être un alias :
    type Error = Box<Debug> lie Debug qui fait que From<T:Debug> for Error conflit avec From<T> for T (réflexif From pour idempotence)

Donc, parce qu'Error ne peut pas implémenter le débogage, vous voudrez peut-être avoir un assistant pour se dérouler dans un résultat avec un débogage transitif :

fn from<T>(result : Result<T>) -> std::result::Result<T, Box<dyn std::fmt::Debug>> { match result { Ok(t) => Ok(t), Err(e) => Err(e.0) } }

Il ne peut pas être impl From<Result<T>> for StdResult<T> ni Into<StdResult> for Result<T> (ne peut pas implémenter le trait en amont pour le type en amont)

Par exemple, vous pouvez l'utiliser pour obtenir un retour de débogage pour la résiliation :

fn main() -> std::result::Result<(), Box<dyn std::fmt::Debug>> { from(run()) }

Mauvaise idée : peut-être que l'opérateur ? pourrait en quelque sorte représenter une liaison monadique, donc

let x: i32 = something?;
rest of function body

devient

Monad::bind(something, fn(x) {
    rest of function body
})

Une idée terrible, mais elle plaît au geek qui sommeille en moi.

@derekdreery Je ne pense pas que cela fonctionnerait bien avec un flux de contrôle impératif comme return et continue

Gardez à l'esprit que la sémantique de l'opérateur ? est déjà stable. Seul le trait Try réel est instable, et toute modification doit préserver le même effet stable pour Option et Result .

@KrishnaSannasi

@derekdreery, je ne pense pas que cela fonctionnerait bien avec un flux de contrôle impératif comme revenir et continuer

Je suis d'accord avec cette affirmation.

@cuviper

Gardez à l'esprit que la sémantique du ? l'opérateur sont déjà stables. Seul le trait Try réel est instable, et tout changement doit préserver le même effet stable pour l'option et le résultat.

Est-ce aussi vrai à travers les époques ?

Sur une note plus générale, je ne pense pas qu'il soit possible d'unifier des concepts comme .await , ?/ early return, Option::map, Result::map, Iterator::map in rust, mais comprendre que tout cela partage une certaine structure m'aide certainement à être un meilleur programmeur.

Excuses d'être OT.

Je ne sais pas si une époque/édition serait autorisée à changer le désucrage de ? , mais cela compliquerait certainement beaucoup de problèmes de cross-crate comme les macros.

Mon interprétation des garanties de stabilité et des époques RFC est que le comportement ? (sucre ou autre) pourrait être modifié, mais son comportement actuel sur les types Option / Result doit rester le même chose parce que briser cela créerait beaucoup plus de désabonnement que nous ne pourrions jamais espérer justifier.

Et si Option était en quelque sorte un alias de type pour Result ? C'est-à-dire type Option<T> = Result<T, NoValue> , Option::<T>::Some(x) = Result::<T, NoValue>::Ok(x) , Option::<T>::None = Result::<T, NoValue>::Err(NoValue) ? Cela prendrait un peu de magie à mettre en œuvre et n'est pas réaliste dans l'environnement linguistique actuel, mais cela vaut peut-être la peine d'être exploré.

Nous ne pouvons pas effectuer ce changement car il existe des implémentations de traits qui reposent sur des types distincts de Option et Result .

Si nous ignorons cela, nous pourrions aller plus loin et même faire de bool un alias pour Result<True, False> , où True et False sont des types d'unités.

A-t-il été envisagé d'ajouter un Try impl pour l'unité () ? Pour les fonctions qui ne renvoient rien, un retour anticipé peut toujours être utile en cas d'erreur dans une fonction non critique, comme un rappel de journalisation. Ou, l'unité a-t-elle été exclue parce qu'il est préférable de ne jamais ignorer les erreurs en silence dans aucune situation ?

Ou, l'unité a-t-elle été exclue parce qu'il est préférable de ne jamais ignorer les erreurs en silence dans aucune situation ?

Cette. Si vous souhaitez ignorer les erreurs dans un contexte non critique, vous devez utiliser unwrap ou l'une de ses variantes.

être capable d'utiliser ? sur quelque chose comme foo() -> () serait très utile dans un contexte de compilation conditionnelle et devrait être fortement pris en compte. Je pense que c'est différent de votre question cependant.

vous devez utiliser unwrap ou l'une de ses variantes.

unwrap() provoque une panique, donc je ne recommanderais pas de l'utiliser dans des contextes non critiques. Cela ne serait pas approprié dans les situations où un simple retour est souhaité. On pourrait faire valoir que ? n'est pas totalement "silencieux" en raison de l'utilisation explicite et visible de ? dans le code source. De cette façon, ? et unwrap() sont analogues pour les fonctions unitaires, avec juste une sémantique de retour différente. La seule différence que je vois est que unwrap() rendra les effets secondaires visibles (abandon du programme / impression d'une trace de pile) et que ? ne le feraient pas.

À l'heure actuelle, l'ergonomie du retour précoce dans les fonctions unitaires est considérablement pire que dans celles qui retournent Result ou Option . Peut-être que cet état de fait est souhaitable car les utilisateurs doivent utiliser un type de retour de Result ou Option dans ces cas, et cela les incite à changer leur API. Dans tous les cas, il peut être utile d'inclure une discussion sur le type de retour d'unité dans le PR ou la documentation.

être capable d'utiliser ? sur quelque chose comme foo() -> () serait très utile dans un contexte de compilation conditionnelle et devrait être fortement pris en compte. Je pense que c'est différent de votre question cependant.

Comment cela fonctionnerait-il ? Juste toujours évaluer à Ok(()) et être ignoré?

Aussi, pouvez-vous donner un exemple de l'endroit où vous voudriez l'utiliser?

Oui, l'idée était que quelque chose comme MyCollection::push pourrait, selon la configuration du temps de compilation, avoir une valeur de retour Result<(), AllocError> ou une valeur de retour () si la collection est configurée pour paniquer en cas d'erreur. Le code intermédiaire utilisant la collection ne devrait pas avoir à y penser, donc s'il pouvait simplement _toujours_ utiliser ? même lorsque le type de retour est () ce serait pratique.

Après presque 3 ans, est-ce que c'est plus près d'être résolu ?

@Lokathor qui ne fonctionnerait que si un type de retour Result<Result<X,Y>,Z> ou similaire n'était pas possible. Mais il est. Donc pas possible.

Je ne comprends pas pourquoi un résultat en couches pose des problèmes. Pourriez-vous s'il vous plaît élaborer?

A des fins de recoupement, une formulation alternative a été proposée sur les internes par @dureuill :

https://internals.rust-lang.org/t/a-slightly-more-general-easier-to-implement-alternative-to-the-try-trait/12034

@Lokathor
D'accord, j'y ai réfléchi plus profondément et je pense que j'aurais peut-être trouvé une assez bonne explication.

Utiliser une annotation

Le problème est que l'interprétation du type de retour ou des annotations étranges encombreraient le code. Ce serait possible, mais cela rendrait le code plus difficile à lire. (Condition préalable : #[debug_result] applique votre comportement souhaité, et modifie une fonction pour paniquer en mode release au lieu de renvoyer un Result::Err(...) , et déroule Result::Ok , mais cette partie est délicate)

#[debug_result]
fn f() -> Result<X, Y>;

#[debug_result]
fn f2() -> Result<Result<A, B>, Y>;

#[debug_result]
fn g() -> Result<X, Y> {
    // #[debug_result_spoiled]
    let w = f();
    // w could have type X or `Result<X,Y>` based on a #[cfg(debug_...)]
    // the following line would currently only work half of the time
    // we would modify the behavoir of `?` to a no-op if #[cfg(not(debug_...))]
    // and `w` was marked as `#[debug_result]`-spoiled
    let x = w?;

    // but it gets worse; what's with the following:
    let y = f2();
    let z = y?;
    // it has completely different sematics based on a #[cfg(debug_...)],
    // but would (currently?) print no warnings or errors at all,
    // and the type of z would be differently.

    Ok(z)
}

Ainsi, cela rendrait le code plus difficile à lire.
Nous ne pouvons pas simplement modifier le comportement de ? simplement en un no-op,
surtout si la valeur de retour d'un #[debug_result] est enregistrée dans une variable temporaire et plus tard "try-propagée" avec l'opérateur ? . Cela rendrait la sémantique du ? vraiment confuse, car cela dépendrait de nombreuses "variables", qui ne sont pas nécessairement connues au "moment de l'écriture" ou pourraient être difficiles à deviner en lisant simplement le code source. #[debug_result] aurait besoin de gâcher les variables auxquelles sont attribuées des valeurs de fonction, mais cela ne fonctionnera pas si une fonction est marquée avec #[debug_result] et une autre ne l'est pas, par exemple ce qui suit serait une erreur de type.

// f3 is not annotated
fn f3() -> Result<X, Y>;

// later inside of a function:
   // z2 needs to be both marked spoiled and non-spoiled -> type error
   let z2 = if a() {
       f3()
   } else {
       f()
   };
   // althrough the following would be possible:
   let z2_ = if a() {
       f3()?
   } else {
       f()?
   };

Alternative : utiliser un nouveau type

Une solution "plus propre" pourrait être un type DebugResult<T, E> qui panique simplement lorsqu'un certain indicateur de compilation est défini et qu'il est construit à partir d'une erreur, mais serait sinon équivalent à Result<T, E> autrement. Mais cela serait également possible avec la proposition actuelle, je pense.

Réponse au dernier message de @zserik : La macro que vous décrivez est inutile - le type de retour de charge de fonction basé sur la configuration de construction

@ tema3210 Je sais. Je voulais seulement souligner que l'idée de @Lokathor ne fonctionnerait généralement pas dans la pratique. La seule chose qui pourrait partiellement fonctionner est la suivante, mais seulement avec de nombreuses restrictions, ce qui, à mon avis, n'en vaut pas la peine à long terme :

// the result of the fn *must* have Try<Ok=()>
// btw. the macro could be already implemented with a proc_macro today.
#[debug_result]
fn x() -> Result<(), XError> {
  /* ..{xbody}.. */
}

// e.g. evaluates roughly to:
//..begin eval
fn x_inner() -> Result<(), XError> {
  /* ..{xbody}.. */
}

#[cfg(panic_on_err)]
fn x() {
  let _: () = x_inner().unwrap();
}
#[cfg(not(panic_on_err))]
fn x() -> Result<(), XError> {
  x_innner()
}

//..end eval

fn y() -> Result<(), YError> {
  /* ... */
  // #[debug_result] results can't be matched on and can't be assigned to a
  // variable, they only can be used together with `?`, which would create
  // an asymetry in the type system, but could otherwise work, althrough
  // usage would be extremely restricted.
  x()?;
  // e.g. the following would fail to compile because capturing the result
  // is not allowed
  if let Err(z) = x() {
    // ...
  }
}

@zserik Est-il possible que cela prenne réellement une forme comme celle-ci?

#[cfg(panic_on_err)]
fn x() -> Result<(), !> {
  let _: () = x_inner().unwrap();
}

#[cfg(not(panic_on_err))]
fn x() -> Result<(), XError> {
  x_innner()
}

OK bonne idée.

Je ne sais pas vraiment si c'est quelque chose qui doit être pris en compte avant la stabilisation, mais je suis intéressé par la mise en œuvre des traces de retour d'erreur et je pense que cela implique des modifications du trait Try ou au moins de son impl fourni pour Result pour le faire fonctionner. Finalement, je prévois de transformer cela en RFC, mais je veux m'assurer que le trait try n'est pas stabilisé de manière à ce qu'il soit impossible de l'ajouter plus tard au cas où cela me prendrait un certain temps pour me mettre à écrire ladite RFC. L'idée de base est la suivante.

Vous avez un trait que vous utilisez pour transmettre des informations de suivi dans lequel utilise la spécialisation et un impl par défaut pour T afin de maintenir la compatibilité descendante

pub trait Track {
    fn track(&mut self, location: &'static Location<'static>);
}

default impl<T> Track for T {
    fn track(&mut self, _: &'static Location<'static>) {}
}

Et puis vous modifiez l'implémentation Try pour Result pour utiliser track_caller et passez cette information dans le type interne,

    #[track_caller]
    fn from_error(mut v: Self::Error) -> Self {
        v.track(Location::caller());
        Self::Err(v)
    }

Et puis pour les types que vous souhaitez collecter des backtraces pour vous implémentez Track

#[derive(Debug, Default)]
pub struct ReturnTrace {
    frames: Vec<&'static Location<'static>>,
}

impl Track for ReturnTrace {
    fn track(&mut self, location: &'static Location<'static>) {
        self.frames.push(location);
    }
}

L'utilisation finit par ressembler à ceci

#![feature(try_blocks)]
use error_return_trace::{MyResult, ReturnTrace};

fn main() {
    let trace = match one() {
        MyResult::Ok(()) => unreachable!(),
        MyResult::Err(trace) => trace,
    };

    println!("{:?}", trace);
}

fn one() -> MyResult<(), ReturnTrace> {
    try { two()? }
}

fn two() -> MyResult<(), ReturnTrace> {
    MyResult::Err(ReturnTrace::default())?
}

Et la sortie d'une version très merdique d'un backtrace ressemble à ceci

ReturnTrace { frames: [Location { file: "examples/usage.rs", line: 18, col: 42 }, Location { file: "examples/usage.rs", line: 14, col: 16 }] }

Et voici une preuve de concept en action https://github.com/yaahc/error-return-traces

Je pensais qu'un seul type d'erreur que nous pouvons convertir pour essayer l'implémenteur de traits pourrait être insuffisant, nous pourrions donc fournir une interface comme celle-ci :

trait Try{
     type Error;
     type Ok;
     fn into_result(self)->Result<Self::Ok,Self::Error>;
     fn from_ok(val: Self::Ok)->Self;
     fn from_error<T>(val: T)->Self;
}

Notez que le compilateur peut monomorphiser from_error , en évitant l'appel de From::from , et on peut fournir manuellement la méthode impl pour différents types d'erreur, ce qui permet à l'opérateur ? de "déballer" différents types d'erreurs.

 fn from_error<T>(val: T)->Self;

Tel qu'il est écrit, l'implémenteur devrait accepter _n'importe quelle_ taille T , sans contrainte. Si vous vouliez laisser cela être contraint de manière personnalisée, il devrait s'agir d'un paramètre de trait, comme Try<T> . Ceci est similaire à la combinaison TryBlock / Bubble<Other> que @scottmcm avait dans https://github.com/rust-lang/rust/issues/42327#issuecomment-457353299 .

Tel qu'il est écrit, l'implémenteur devrait accepter _n'importe quelle_ taille T , sans contrainte. Si vous vouliez laisser cela être contraint de manière personnalisée, il devrait s'agir d'un paramètre de trait, comme Try<T> . Ceci est similaire à la combinaison TryBlock / Bubble<Other> que @scottmcm avait dans #42327 (commentaire) .

Je voulais dire que l'utilisation devrait ressembler à ceci :

trait Try{
     //same from above
}
struct Dummy {
    a: u8,
}
struct Err1();
struct Err2();
impl Try for Dummy {
    type Ok=();
    type Error=();

    fn into_result(self)->Result<Self::Ok,Self::Error>{
        std::result::Result::Ok(())
    }
    fn from_ok(val: Self::Ok)->Self{
        Self{a: 0u8}
    }
    fn from_error<T>(val: Err1)->Self where T == Err1{
        Self{a: 1u8}
    }
    fn from_error<T>(val: Err2)->Self where T == Err2{
        Self{a: 2u8}
    }
}

vous auriez besoin de diviser le Try et le TryFromError. J'aime ça plus que la proposition originale, mais je pense qu'il faudrait un nouveau RFC.

(et je pense toujours que cela aurait dû s'appeler "propager" au lieu de "essayer", mais je m'éloigne du sujet)

@ tema3210 Je pense que je comprends votre intention, mais ce n'est pas Rust valide.

@SoniEx2

vous auriez besoin de diviser le Try et le TryFromError.

C'est pourquoi j'ai mentionné le design TryBlock / Bubble<Other> . Nous pouvons débattre de ce choix de nom, mais l'idée était que le retour anticipé n'est pas toujours une question d'erreurs en soi. Par exemple, de nombreuses méthodes internes Iterator utilisent un type LoopState . Pour quelque chose comme find , ce n'est pas une erreur de trouver ce que vous cherchez, mais nous voulons arrêter l'itération là-bas.

https://github.com/rust-lang/rust/blob/3360cc3a0ea33c84d0b0b1163107b1c1acbf2a69/src/libcore/iter/mod.rs#L375 -L378

Nous pouvons débattre de ce choix de nom, mais l'idée était que le retour anticipé n'est pas toujours une question d'erreurs en soi.

précisément pourquoi je n'aime pas le nom "try" et préférerais le nom "propagate", car il "propage" un retour "early" :p

(je ne sais pas si cela a du sens ? La dernière fois que j'ai évoqué cela, la chose "propager" n'avait de sens pour moi que pour une raison quelconque et je n'ai jamais été tout à fait capable de l'expliquer aux autres.)

Cette caractéristique sera-t-elle utile lorsque vous tenterez d'écraser le comportement par défaut de ? pour ajouter un crochet de journalisation pour consigner les informations de débogage (comme le numéro de fichier/de ligne) ?

Actuellement, il est possible d'écraser les macros stdlib, mais il semble que l'opérateur ? ne soit pas converti explicitement en macro try! . C'est malheureux.

@stevenroose Pour ajouter la prise en charge de cela uniquement au trait Try , il faudrait une modification du trait Try pour inclure des informations sur l'emplacement du fichier sur l'emplacement où ? "s'est produit" .

@stevenroose Pour ajouter la prise en charge de cela uniquement au trait Try , il faudrait une modification du trait Try pour inclure des informations sur l'emplacement du fichier sur l'emplacement où ? "s'est produit" .

Ce n'est pas vrai, #[track_caller] peut être utilisé sur les traits impls même si la définition du trait n'inclut pas l'annotation

@stevenroose pour répondre à votre question, oui, mais si vous souhaitez imprimer tous les emplacements ? une erreur se propage, vous devriez vérifier le commentaire de trace de retour d'erreur ci-dessus

https://github.com/rust-lang/rust/issues/42327#issuecomment -619218371

Je ne sais pas si quelqu'un l'a déjà mentionné, allons-nous impl Try for bool , peut-être Try<Ok=(), Error=FalseError> ?
Pour que nous puissions faire une chose comme ça

fn check_key(v: Vec<A>, key: usize) -> bool {
  let x = v.get_mut(key)?; // Option
  x.is_valid()?; // bool
  x.transform()?; // Result
  true
}

Maintenant, je dois utiliser le type de retour Option<()> dans la plupart des cas où je pense que ? pourrait rendre le code beaucoup plus clair.

Je ne sais pas si quelqu'un l'a déjà mentionné, allons-nous impl Try for bool , peut-être Try<Ok=(), Error=FalseError> ?

Cela ferait en sorte que Try se comporte comme l'opérateur && sur les bool s. Comme d'autres l'ont souligné ci-dessus, il existe également des cas d'utilisation pour le court-circuit en cas de succès, auquel cas Try devrait se comporter comme || . Étant donné que les opérateurs && et || ne sont pas beaucoup plus longs à taper, je ne vois pas non plus grand avantage à avoir cette implémentation.

@calebsander merci de bien vouloir répondre.
C'est vrai pour certains cas simples, mais je ne pense pas que ce soit souvent le cas, en particulier nous ne pourrions jamais avoir d'instruction d'affectation comme let x = v.get_mut(key)? dans l'expression.
Si && et || faisaient toujours l'affaire, on pourrait aussi bien jouer avec .unwrap_or_else() , .and_then() pour Option et Error cas.
Pourriez-vous exprimer le code qui coule en && et || ?

fn check_key(v: Vec<A>, key: usize) -> bool {
  let x = v.get_mut(key)?; // Option
  x.not_valid().not()?; // bool
  for i in x.iter() {
    if i == 1 { return true }
    if i == 2 { return false }
    debug!("get {}" i);
  }
  let y = x.transform()?; // Result
  y == 1
}

Pour certaines conditions où true signifie échec alors que false signifie succès, !expr? peut prêter à confusion, mais nous pourrions utiliser expr.not()? pour faire le tour (remarque : pour ops::Try , nous avons toujours false pour Error )

C'est vrai pour certains cas simples, mais je ne pense pas que ce soit souvent le cas, en particulier nous ne pourrions jamais avoir d'instruction d'affectation comme let x = v.get_mut(key)? dans l'expression.

Eh bien, à moins que je ne comprenne mal votre proposition, la simple mise en œuvre de Try<Ok = (), Error = FalseError> pour bool ne permettrait pas cela. Vous auriez également besoin de impl From<NoneError> for FalseError pour que l'opérateur ? puisse convertir None en false . (Et de même, si vous voulez appliquer ? à un Result<T, E> intérieur d'une fonction qui renvoie bool , vous auriez besoin de impl From<E> for FalseError . Je pense qu'un tel l'implémentation globale serait problématique.) Vous pouvez également utiliser some_option().ok_or(false)? et some_result().map_err(|_| false)? si vous êtes d'accord avec la valeur de retour Result<bool, bool> (que vous pouvez réduire avec .unwrap_or_else(|err| err) ).

Laissant de côté les problèmes de conversion d'autres erreurs Try en bool , l'implémentation Try vous proposez pour bool n'est que l'opérateur && . Par exemple, ce sont des équivalents

fn using_try() -> bool {
    some_bool()?;
    some_bool()?;
    some_bool()
}

et

fn using_and() -> bool {
    some_bool() &&
    some_bool() &&
    some_bool()
}

(Certes, les flux de contrôle comme if et loop qui renvoient non- () sont moins simples à traduire.)

Pour certaines conditions où true signifie échec alors que false signifie succès, !expr? peut être déroutant, mais nous pourrions utiliser expr.not()? pour faire le tour (remarque : pour ops::Try , nous avons toujours false pour Error )

Il n'est pas clair pour moi que false devrait représenter le cas Error . (Par exemple, Iterator.all() voudrait que false court-circuite, mais Iterator::any() voudrait plutôt true .) Comme vous l'avez souligné, vous pouvez obtenir le comportement de court-circuit opposé en inversant la valeur passée à ? (et en inversant également la valeur de retour de la fonction). Mais je ne pense pas que cela donne un code très lisible. Il pourrait être plus logique d'avoir des struct séparés And(bool) et Or(bool) qui implémentent les deux comportements différents.

Et de même, si vous souhaitez postuler ? à un résultatà l'intérieur d'une fonction qui renvoie bool, vous auriez besoin d'implpour FalseError. Je pense qu'une telle mise en œuvre globale serait problématique.

Non, je ne veux pas impl From<T> for FalseError , on pourrait peut-être faire result.ok()?

Il n'est pas clair pour moi que false devrait représenter le cas d'erreur.

Je pense que c'est quelque peu naturel quand nous avons bool::then qui mappent true à Some .

Pardonnez-moi si je l'ai raté - pourquoi NoneError impl std::error::Error ? Cela le rend inutile pour toute fonction renvoyant Box<dyn Error> ou similaire.

Pardonnez-moi si je l'ai raté - pourquoi NoneError impl std::error::Error ? Cela le rend inutile pour toute fonction renvoyant Box<dyn Error> ou similaire.

Pas un expert ici, mais je peux voir des problèmes importants en essayant de trouver un message d'erreur utile pour "quelque chose était None et vous vous attendiez à ce qu'il soit Some " (ce qui est essentiellement ce que vous gagnerais ici - un message de diagnostic). D'après mon expérience, il a toujours été plus logique d'utiliser le combinateur Option::ok_or_else pour utiliser un autre type d'erreur à la place, car en tant que code appelant, j'ai généralement un bien meilleur contexte à donner de toute façon.

Je suis d'accord avec @ErichDonGubler. C'est super ennuyeux de ne pas pouvoir utiliser ? avec Option , mais c'est pour une bonne raison, et, en tant qu'utilisateur du langage, je pense qu'il est dans l'intérêt de tous de gérer correctement les erreurs. Faire ce travail supplémentaire de lier le contexte d'erreur à None au lieu de simplement faire ? est vraiment, vraiment ennuyeux, mais cela force à communiquer correctement les erreurs, ce qui en vaut la peine.

La seule fois où j'autoriserais l'interprétation de None comme une erreur, c'est lors d'un prototypage très approximatif. J'ai pensé que si nous ajoutions quelque chose comme un appel Option::todo_err() , cela renverrait Ok de la valeur interne, mais paniquerait sur None . C'est très contre-intuitif jusqu'à ce que vous analysiez les besoins du mode "prototypage approximatif" de la création de code. C'est très similaire à Ok(my_option.unwrap()) , mais n'a pas besoin d'un Ok(...) wrapping. De plus, il spécifie explicitement la nature "à faire", communiquant ainsi l'intention de supprimer ce code de la logique de production, en le remplaçant par une liaison d'erreur appropriée.

mais paniquera sur Aucun

Je pense fortement que nous devrions simplement laisser cela à unwrap . Si nous avons ajouté un todo_err, il devrait renvoyer un type d'erreur réel, ce qui soulève la question de savoir quel type d'erreur retourner.

De plus, je soupçonne que fn todo_err(self) -> Result<Self, !> aurait le problème évident de nécessiter ! pour se stabiliser, ce qui, euh, eh bien, un jour.

Mon cas d'utilisation est en effet un prototypage approximatif où je ne me soucie pas trop des erreurs. L'ensemble de NoneError souffre-t-il pas de ces problèmes que vous avez énumérés ? S'il est décidé qu'il devrait exister (ce qui je pense est une bonne chose, au moins pour le prototypage), je pense qu'il devrait implémenter Error car il est nommé pour en être un.

En raison du type dépourvu de cet impl, j'ai eu recours à un .ok_or("error msg") dessus, ce qui fonctionne aussi mais est moins pratique.

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