Rust: Problème de suivi pour RFC 1937 : `?` dans `main`

Créé le 18 juil. 2017  ·  183Commentaires  ·  Source: rust-lang/rust

Il s'agit d'un problème de suivi pour la RFC " ? in main " (rust-lang/rfcs#1937).

Pas:

Stabilisations :

  • [x] Stabiliser main avec des types de retour non-() (https://github.com/rust-lang/rust/issues/48453) Fusionné dans https://github.com/rust-lang/ rouille/pull/49162
  • [x] Stabiliser les tests unitaires avec des types de retour non-() (https://github.com/rust-lang/rust/issues/48854)

Problèmes liés:

  • [x] Le message d'erreur pour les tests unitaires n'est pas génial (https://github.com/rust-lang/rust/issues/50291)

Questions non résolues :

B-RFC-approved C-tracking-issue E-mentor T-compiler T-lang WG-compiler-middle

Commentaire le plus utile

Toutes mes excuses pour intervenir à un moment où il est probablement trop tard pour faire quoi que ce soit à ce sujet, mais je voulais laisser mes commentaires ici au cas où il y en aurait. J'ai lu la majeure partie de ce fil, donc je parle avec ce contexte à l'esprit. Cependant, ce fil est long, donc s'il semble que j'ai oublié quelque chose, alors je l'ai probablement fait et j'apprécierais qu'il me le signale. :-)

TL; DR - Je pense que montrer le message Debug d'une erreur était une erreur, et qu'un meilleur choix serait d'utiliser le message Display d'une erreur.

Au cœur de ma conviction est que, en tant que personne qui _construit régulièrement des programmes CLI dans Rust_, je ne me souviens pas m'être jamais beaucoup soucié de ce qu'est le message Debug d'un Error . À savoir, le Debug d'une erreur est, par conception, pour les développeurs, pas pour les utilisateurs finaux. Lorsque vous construisez un programme CLI, son interface est fondamentalement destinée à être lue par les utilisateurs finaux, donc un message Debug a très peu d'utilité ici. Autrement dit, si un programme CLI que j'ai envoyé aux utilisateurs finaux affichait la représentation de débogage d'une valeur Rust en fonctionnement normal, je considérerais qu'il s'agit d'un bogue à corriger. Je pense généralement que cela devrait être vrai pour tous les programmes CLI écrits en Rust, bien que je comprenne que cela puisse être un point sur lequel des personnes raisonnables peuvent être en désaccord. Cela dit, une implication quelque peu surprenante de mon avis est que nous avons effectivement stabilisé une fonctionnalité où son mode de fonctionnement par défaut vous lance avec un bogue (encore une fois, IMO) qui devrait être corrigé.

En montrant la représentation Debug d'une erreur par défaut, je pense aussi que nous encourageons les mauvaises pratiques. En particulier, il est très courant au cours de l'écriture d'un programme Rust CLI d'observer que même l' Display d'une erreur n'est pas assez bonne pour être consommée par les utilisateurs finaux, et qu'un véritable travail doit être fait pour répare le. Un exemple concret de ceci est io::Error . Afficher un io::Error sans chemin de fichier correspondant (en supposant qu'il provienne de la lecture/écriture/ouverture/création d'un fichier) est fondamentalement un bogue, car il est difficile pour un utilisateur final de faire quoi que ce soit avec. En choisissant d'afficher la Debug d'une erreur par défaut, nous avons rendu plus difficile la découverte de ce type de bogues par les créateurs de programmes CLI. (En plus de cela, le Debug d'un io::Error est beaucoup moins utile que son Display , mais cela en soi n'est pas un gros problème d'après mon expérience. )

Enfin, pour compléter mon propos, j'ai aussi du mal à imaginer les circonstances dans lesquelles j'utiliserais ?-in-main même dans des exemples. À savoir, j'ai essayé d'écrire des exemples qui correspondent le plus possible aux programmes du monde réel, et cela a généralement impliqué d'écrire des choses comme ceci :

use std::error::Error;
use std::process;

fn try_main() -> Result<(), Box<Error>> {
    // do stuff with `?`
}

fn main() {
    if let Err(err) = try_main() {
        eprintln!("{}", err);
        process::exit(1);
    }
}

À première vue, ce serait _charmant_ de le remplacer par ?-in-main , mais je ne peux pas, car il n'affichera pas les Display d'une erreur. Autrement dit, lors de l'écriture d'un vrai programme CLI, j'utiliserai en fait l'approche ci-dessus, donc si je veux que mes exemples reflètent la réalité, alors je pense que je devrais montrer ce que je fais dans de vrais programmes et ne pas prendre de raccourcis (dans une mesure raisonnable ). Je pense en fait que ce genre de chose est vraiment important, et un effet secondaire de cela historiquement était qu'il montrait aux gens comment écrire du code Rust idiomatique sans saupoudrer unwrap partout. Mais si je reviens à l'utilisation ?-in-main dans mes exemples, alors j'ai juste renié mon objectif : j'installe maintenant des gens qui ne savent peut-être pas mieux écrire des programmes qui, par défaut, émettent très messages d'erreur inutiles.

Le modèle "émettre un message d'erreur et quitter avec un code d'erreur approprié" est en fait utilisé dans les programmes raffinés. Par exemple, si ?-in-main utilisait Display , alors je pourrais fusionner les fonctions main et run dans ripgrep aujourd'hui :

https://github.com/BurntSushi/ripgrep/blob/64317bda9f497d66bbeffa71ae6328601167a5bd/src/main.rs#L56 -L86

Bien sûr, je pourrais utiliser ?-in-main à l'avenir en fournissant mon propre impl pour le trait Termination une fois que cela se stabilise, mais pourquoi devrais-je prendre la peine de le faire si je pouvais simplement écrire le main fonction impl dans les exemples afin qu'ils correspondent à la réalité, et à ce stade, je pourrais aussi bien m'en tenir aux exemples que j'ai aujourd'hui (en utilisant un main et un try_main ).

À première vue, il semble que résoudre ce problème serait un changement radical. C'est-à-dire que ce code compile aujourd'hui sur Rust stable :

#[derive(Debug)]
struct OnlyDebug;

fn main() -> Result<(), OnlyDebug> {
    Err(OnlyDebug)
}

Je pense que passer à Display casserait ce code. Mais je n'en suis pas sûr ! S'il s'agit vraiment d'un problème de bateau a navigué, alors je comprends et il n'est pas trop utile d'insister sur ce point, mais je suis assez convaincu à ce sujet pour au moins dire quelque chose et voir si je ne peux pas convaincre les autres et voir s'il y a quoi que ce soit qui puisse être fait pour y remédier. (Il est également tout à fait possible que je réagisse de manière excessive ici, mais jusqu'à présent, quelques personnes m'ont demandé "pourquoi n'utilisez-vous pas ?-in-main ?" dans mes exemples CSV, et ma réponse a été essentiellement, "Je ne vois pas comment il serait possible de faire cela." Peut-être que ce n'était pas un problème qui devait être résolu par ?-in-main , mais certaines personnes ont certainement eu cette impression. Avec son implémentation actuelle , je pourrais le voir utile dans les tests doc et les tests unitaires, mais j'ai du mal à penser à d'autres situations dans lesquelles je l'utiliserais.)

Tous les 183 commentaires

Comment les statuts de sortie vont-ils être traités ?

Ce commentaire de @Screwtapello semble avoir été fait trop près de la fin du FCP pour que des modifications soient apportées au RFC en réponse.

En bref : la RFC propose de renvoyer 2 en cas d'échec pour des motifs qui, bien que fondés, sont obscurs et produisent un résultat légèrement inhabituel ; la chose la moins surprenante est de renvoyer 1 lorsque le programme n'a aucune indication qu'il souhaite plus de détails que le succès ou l'échec. Est-ce suffisamment vélo pour pouvoir en discuter sans avoir l'impression que nous pervertissons le processus RFC, ou sommes-nous maintenant enfermés dans ce détail de mise en œuvre spécifique ?

Ce n'est pas un détail d'implémentation, n'est-ce pas ?

Certains scripts utilisent des codes de sortie pour obtenir des informations d'un sous-processus.

Cela concerne spécifiquement le cas où un sous-processus (implémenté dans Rust) n'a aucune information à donner, au-delà d'un binaire "tout va bien"/"quelque chose s'est mal passé".

Certains scripts utilisent des codes de sortie pour obtenir des informations d'un sous-processus.

Ce comportement est toujours extrêmement dépendant du fait que le programme est appelé _except_ dans la mesure où un non-zéro signifie un échec. Étant donné que std::process::exit avec un wrapper de fonction principale et une table de recherche restera la meilleure option pour ceux qui veulent un statut de sortie plus articulé, peu importe ce qui est fait, cela semble être un détail pour la plupart insignifiant.

Je ne pense pas que SemVer ait une exception de "détail pour la plupart insignifiant".

Je pense que le code de sortie devrait être ajouté à la liste des questions non résolues. @zackw a également ouvert un fil de discussion interne connexe.

Beaucoup de gens conviennent que le code de sortie devrait être 1 en cas d'échec (au lieu de 2 ):
https://www.reddit.com/r/rust/comments/6nxg6t/the_rfc_using_in_main_just_got_merged/

@ arielb1 allez-vous implémenter ce rfc ?

@bkchr

Non, juste pour le guider. J'ai assigné pour ne pas oublier d'écrire les notes de mentorat.

Ahh sympa, ça m'intéresserait de le faire :)
Mais je ne sais pas par où commencer :D

@bkchr

C'est pourquoi je suis ici :-). Je devrais écrire les instructions de mentorat assez tôt.

D'accord, alors j'attends vos instructions.

Consignes de mentorat

Il s'agit d'un problème [WG-compiler-middle]. Si vous souhaitez demander de l'aide, vous pouvez rejoindre #rustc sur irc.mozilla.org (je suis arielby) ou https://gitter.im/rust-impl-period/WG-compiler-middle (je suis @arielb1 là).

Il y a un fichier readme du compilateur WIP au # 44505 - il décrit certaines choses dans le compilateur.

Plan de travail pour ce RFC :

  • [ ] - ajouter l'élément lang Termination à libcore
  • [ ] - autorise l'utilisation Termination dans main
  • [ ] - permet d'utiliser Termination dans les doctests
  • [ ] - permet d'utiliser Termination dans #[test]

ajouter l'élément lang Termination à libcore

Tout d'abord, vous devez ajouter le trait Termination à libcore/ops/termination.rs , ainsi que de la documentation. Vous devrez également le marquer comme instable avec un attribut #[unstable(feature = "termination_trait", issue = "0")] - cela empêchera les gens de l'utiliser avant qu'il ne soit stabilisé.

Ensuite, vous devez le marquer en tant que lang-item dans src/librustc/middle/lang_items.rs . Cela signifie que le compilateur peut le découvrir lors de la vérification de type main (par exemple, voir 0c3ac648f85cca1e8dd89dfff727a422bc1897a6).
Cela signifie:

  1. en l'ajoutant à la liste lang-items (en librustc/middle/lang_items.rs )
  2. en ajoutant un #[cfg_attr(not(stage0), lang = "termination")] au trait Termination . La raison pour laquelle vous ne pouvez pas simplement ajouter un attribut #[lang = "termination"] est que le compilateur "stage0" (pendant le démarrage) ne saura pas que termination est quelque chose qui existe, donc il ne pourra pas compilez libstd. Nous supprimerons manuellement les cfg_attr lors de la mise à jour du compilateur stage0.
    Voir les documents d'amorçage sur XXX pour plus de détails.

autoriser l'utilisation Termination dans main

C'est la partie intéressante que je sais traiter. Cela signifie faire un main qui renvoie () sans vérification de type (actuellement, vous obtenez une erreur main function has wrong type ) et fonctionner.

Pour effectuer une vérification de type, vous devez d'abord supprimer l'erreur existante dans :
https://github.com/rust-lang/rust/blob/9a00f3cc306f2f79bfbd54f1986d8ca7a74f6661/src/librustc_typeck/lib.rs#L171 -L218

Ensuite, vous devez ajouter une vérification que le type de retour implémente le trait Termination dans (vous ajoutez une obligation de trait en utilisant register_predicate_obligation - recherchez les utilisations de cela). Cela peut se faire ici :
https://github.com/rust-lang/rust/blob/9a00f3cc306f2f79bfbd54f1986d8ca7a74f6661/src/librustc_typeck/check/mod.rs#L1100 -L1108

L'autre partie est de le faire fonctionner. Cela devrait être plutôt facile. Comme le dit la RFC, vous voulez rendre lang_start générique sur le type de retour.

lang_start est actuellement défini ici :
https://github.com/rust-lang/rust/blob/9a00f3cc306f2f79bfbd54f1986d8ca7a74f6661/src/libstd/rt.rs#L32

Vous devrez donc le modifier pour qu'il soit générique et corresponde à la RFC :

#[lang = "start"]
fn lang_start<T: Termination>
    (main: fn() -> T, argc: isize, argv: *const *const u8) -> !
{
    use panic;
    use sys;
    use sys_common;
    use sys_common::thread_info;
    use thread::Thread;

    sys::init();

    sys::process::exit(unsafe {
        let main_guard = sys::thread::guard::init();
        sys::stack_overflow::init();

        // Next, set up the current Thread with the guard information we just
        // created. Note that this isn't necessary in general for new threads,
        // but we just do this to name the main thread and to give it correct
        // info about the stack bounds.
        let thread = Thread::new(Some("main".to_owned()));
        thread_info::set(main_guard, thread);

        // Store our args if necessary in a squirreled away location
        sys::args::init(argc, argv);

        // Let's run some code!
        let exitcode = panic::catch_unwind(|| main().report())
            .unwrap_or(101);

        sys_common::cleanup();
        exitcode
    });
}

Et ensuite, vous devrez l'appeler à partir de create_entry_fn . Actuellement, il instancie un lang_start monomorphe en utilisant Instance::mono , et vous devrez le changer pour utiliser monomorphize::resolve avec les bons sous-ensembles.

https://github.com/rust-lang/rust/blob/9a00f3cc306f2f79bfbd54f1986d8ca7a74f6661/src/librustc_trans/base.rs#L697

autoriser l'utilisation Termination dans les doctests

Je ne comprends pas vraiment comment fonctionnent les doctests. Peut-être demander à @alexcrichton (c'est ce que je ferais) ?

autoriser l'utilisation Termination dans #[test]

Je ne comprends pas vraiment comment fonctionne libtest. Peut-être demander à @alexcrichton (c'est ce que je ferais) ? Les tests unitaires sont essentiellement générés par une macro, vous devez donc modifier cette macro, ou son appelant, pour gérer les types de retour qui ne sont pas () .

@bkchr

Pouvez-vous au moins rejoindre l'IRC/gitter ?

@bkchr vient de vérifier - je vous ai vu et @ arielb1 converser sur gitter il y a quelque temps, des progrès? Se faire sucer quelque part ?

Non désolé, aucun progrès jusqu'à présent. Actuellement, j'ai beaucoup de choses à faire, mais j'espère que je trouverai un peu de temps cette semaine pour commencer là-dessus.

@bkchr Si vous avez besoin d'aide, faites-le moi savoir !

Je suis actuellement un peu coincé, je veux créer l'Obligation. Pour créer l'obligation j'ai besoin d'un TraifRef, pour un TraitRef j'ai besoin d'un DefId. Quelqu'un peut-il m'indiquer un code sur la façon de créer un DefId à partir du trait de terminaison ?

@bkchr Le trait doit être ajouté à la liste des éléments lang, par exemple : https://github.com/rust-lang/rust/blob/ade0b01ebf18550e41d24c6e36f91afaccd7f389/src/librustc/middle/lang_items.rs#L312
et être marqué avec #[termination_trait] , par exemple : https://github.com/rust-lang/rust/blob/ade0b01ebf18550e41d24c6e36f91afaccd7f389/src/libcore/fmt/mod.rs#L525 -L526

Ouais ce n'est pas le problème, je l'ai déjà fait. Je dois vérifier le trait de terminaison dans la fonction check_fn. Je veux utiliser register_predicate_obligation et pour cela j'ai besoin du defid du trait de terminaison.

Oh, alors tout ce dont vous avez besoin est tcx.require_lang_item(TerminationTraitLangItem) .

@bkchr comment ça va? Je viens juste de me reconnecter. =) Ne vous inquiétez pas si vous êtes occupé, assurez-vous simplement que vous obtenez toute l'aide dont vous avez besoin.

Désolé, occupé en ce moment :/ Jusqu'à présent, j'ai reçu toute l'aide dont j'avais besoin :)

Voici le code à vérifier pour le TerminationTrait : https://github.com/bkchr/rust/blob/f185e355d8970c3350269ddbc6dfe3b8f678dc44/src/librustc_typeck/check/mod.rs#L1108

Je pense que je ne vérifie pas le type de retour de la fonction? J'obtiens l'erreur suivante :

error[E0277]: the trait bound `Self: std::ops::Termination` is not satisfied
  --> src/rustc/rustc.rs:15:11
   |
15 | fn main() { rustc_driver::main() }
   |           ^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::ops::Termination` is not implemented for `Self`
   |
   = help: consider adding a `where Self: std::ops::Termination` bound

Que dois-je changer, pour vérifier le type de retour de la fonction ?

@bkchr Je recommanderais de rejoindre le gitter du groupe de travail compilateur-middle sur https://gitter.im/rust-impl-period/WG-compiler-middle pour obtenir des commentaires, ainsi que d'essayer le canal IRC #rust-internals sur https ://chat.mibbit.com/?server=irc.mozilla.org%3A%2B6697&channel=%23rust-internals . :)

@bstrie ouais merci, je fais déjà partie du chat gitter et je pourrais résoudre mon problème. :)

@bkchr votre problème est sur cette ligne . La référence de trait que vous souhaitez créer est quelque chose comme R: TerminationR est le type de retour de la fonction. Ceci est spécifié en construisant un "substs" approprié, qui est l'ensemble de valeurs à substituer aux paramètres de type du trait (dans ce cas, Self ).

Cependant, vous appelez la méthode Substs::identity_for_item sur le trait. Cela vous rendra les substitutions que l'on utiliserait à l'intérieur de la définition de trait elle-même . c'est-à-dire que dans ce cas, vous mappez le paramètre Self déclaré sur le trait Termination à Self . Ce serait approprié si vous vérifiiez la définition d'une fonction à l'intérieur du trait Terminator , mais pas tellement ici.

Ce que vous voulez à la place, c'est obtenir le type de retour de la fonction d'entrée. Ce n'est qu'une des variables ret_ty ou actual_ret_ty . L'un ou l'autre est bien, mais je suppose que ret_ty est meilleur - cela correspond au type de retour déclaré par l'utilisateur (alors que actual_ret_ty est le type renvoyé par le code réel).

Vous pouvez créer les substs que vous voulez en appelant simplement la méthode mk_substs() depuis le tcx. Dans ce cas, il n'y a qu'un seul paramètre, le type, donc quelque chose comme let substs = fcx.tcx.mk_substs(&[ret_ty]); fonctionnerait, je pense.

Je crois que la chose à utiliser est tcx.mk_substs_trait(ret_ty, &[]) .

@bkchr vient de s'enregistrer - a-t-il eu la chance de mettre ce conseil en pratique ? (De plus, pour des réponses plus rapides, il peut être judicieux de demander sur gitter .)

Ouais, je pourrais résoudre le problème avec gitter :)

@bkchr comment ça va? Je viens de vérifier.

Tout va bien, j'aurai probablement du temps cette semaine pour examiner le code.

Y a-t-il de la place pour une personne de plus pour aider à cela ? J'aimerais commencer à contribuer à la communauté Rust avant la fin de l'année et j'aimerais aider avec cette fonctionnalité. J'espère que ce ne serait pas trop déroutant d'avoir deux personnes collaborant là-dessus.

@U007D

C'est une petite fonctionnalité et @bkchr en a presque fini.

Ah, d'accord, c'est bon à savoir, merci. Je garderai un œil sur autre chose pour lequel je peux aider.

@U007D Avez-vous vu https://www.rustaceans.org/findwork ?

@lnicola Oui, j'ai! J'essaie de trouver quelque chose à l'intersection de quelque chose sur lequel je me sens confiant de pouvoir travailler (c'est-à-dire être un net positif) et qui me passionne. Pour être honnête, même si j'apprends Rust depuis environ un an, c'est toujours un peu intimidant de se porter volontaire pour quelque chose. FWIW, ce n'est en aucun cas la faute de la communauté Rust - la communauté Rust s'est pliée en quatre pour en faire une culture ouverte, accueillante et inclusive - la meilleure que j'ai eu le plaisir de vivre. (Je soupçonne que cela a plus à voir avec les vieilles cicatrices de bataille d'années et d'années d'expérience dans l'industrie technologique où les équipes ont tendance à être compétitives plutôt que collaboratives.)

Quoi qu'il en soit, mon objectif est de choisir quelque chose cette année et de commencer au moins à apporter une contribution positive. Il est temps pour moi de m'impliquer ! :)

Merci pour la suggestion, @lnicola. C'est une bonne ressource.

@bkchr des mises à jour ?

Je suis dessus (https://github.com/rust-lang/rust/pull/46479). Maintenant, j'ai des vacances et du temps pour travailler dans les commentaires de la pull request. Désolé pour tout le retard :/

Oh, désolé, je n'avais pas remarqué que vous aviez une demande d'extraction. Il a été réticulé.

Bonjour, euh. J'ai donc pensé que je commencerais ma carrière de contributeur potentiel de Rust en faisant du bikeshedding, comme le veut la tradition. Plus précisément, à propos de celui-ci :

  • [ ] Le nom du trait introduit

Que diriez-vous Exit ? C'est court et précis, correspondant au vocabulaire Rust existant. Exit-as-a-nom est un équivalent naturel de exit-as-a-verb qui, pour la plupart, est le mot familier pour mettre fin à un processus "de l'intérieur" de manière contrôlée.

Pour un programmeur C++ en particulier, "terminaison" évoque std::terminate qui par défaut est une terminaison anormale (appelant abort ) et est fondamentalement l'équivalent C++ d'une panique (mais contrairement à une panique, ne déroule jamais le empiler).

Attendez, ignorez ce commentaire, on dirait que le RFC a laissé cela explicitement ouvert à la discussion.

J'aime bien Exit comme nom de trait.

Je pense que la fonctionnalité sera stabilisée bien avant le trait, comme cela s'est produit avec Carrier .

FWIW c'est un autre cas où je suis vraiment content que le nom provisoire ait été changé avant la stabilisation :D

En tant qu'auteur de la RFC, je n'ai aucune objection à changer le nom du trait en Exit , ou quoi que ce soit d'autre vraiment. Je ne suis pas particulièrement doué pour nommer les choses et heureux que quelqu'un d'autre ait une meilleure idée.

https://github.com/rust-lang/rust/blob/5f7aeaf6e2b90e247a2d194d7bc0b642b287fc16/src/libstd/lib.rs#L507

Le trait est-il censé être

  1. placé dans libstd au lieu de libcore, et
  2. vient d'appeler std::Termination , pas std::ops::Termination ?

Le trait n'a pas pu être placé dans libcore , car l'implémentation pour Result nécessite d'imprimer dans stderr et cela ne peut pas être fait dans libcore .

@bkchr L'impl étant dans libstd ne signifie pas que le trait doit également être dans libstd.

@kennytm Je sais, mais le résultat est également défini dans libcore, donc la terminaison ne peut pas être implémentée pour le résultat dans libstd.

@zackw +1 vote de plus pour Exit comme nom de trait.

@U007D : Pourriez-vous s'il vous plaît utiliser le bouton de réactions (par exemple, 👍) au lieu de poster un tel message ? Cela vous permettrait d'éviter d'ennuyer les abonnés en les cinglant inutilement.

Puis-je enregistrer libtest / libsyntax , si un language_feature est activé (dans une caisse) ? @arielb1 @nikomatsakis @alexcrichton

@bkchr dans libsyntax, vous devrez peut-être le transmettre, mais c'est théoriquement possible, mais dans libtest lui-même au moment de l'exécution, je ne pense pas que vous puissiez vérifier.

@bkchr comment ça se passe ici?

J'y travaille encore, mais pour le moment je n'ai plus de questions :)

Je pense que cet impl est trop strict:

#[unstable(feature = "termination_trait", issue = "43301")]
impl<T: Termination, E: Error> Termination for Result<T, E> {
    fn report(self) -> i32 {
        match self {
            Ok(val) => val.report(),
            Err(err) => {
                print_error(err);
                exit::FAILURE
            }
        }
    }
}


#[unstable(feature = "termination_trait", issue = "43301")]
fn print_error<E: Error>(err: E) {
    eprintln!("Error: {}", err.description());

    if let Some(ref err) = err.cause() {
        eprintln!("Caused by: {}", err.description());
    }
}

Il existe plusieurs erreurs couramment utilisées qui n'implémentent pas Error , principalement Box<::std::error::Error> et failure::Error . Je pense aussi que c'est une erreur d'utiliser la méthode description au lieu de l'impl d'affichage de cette erreur.

Je proposerais de remplacer cet impl par cet impl plus large:

#[unstable(feature = "termination_trait", issue = "43301")]
impl<T: Termination, E: Display> Termination for Result<T, E> {
    fn report(self) -> i32 {
        match self {
            Ok(val) => val.report(),
            Err(err) => {
                eprintln!("Error: {}", err)
                exit::FAILURE
            }
        }
    }
}

Cela fait perdre la chaîne des causes, ce qui est une déception.

Utiliser l'impl d'affichage plutôt que la description est certainement la meilleure chose à faire.

La chaîne des causes est un problème intéressant. En particulier, cette implémentation n'imprime que les deux premiers membres de la chaîne de causes.

l'échec a dû gérer la chaîne de causes et s'est installé sur ce comportement par défaut (par exemple, si vous créez simplement des erreurs en utilisant .context :

  • {} imprime uniquement cette erreur
  • {:?} imprime cette erreur ainsi que sa cause (récursivement)

Nous pourrions décider d'utiliser :? ici et de le déboguer au lieu d'afficher. Incertain.

Ouais, je sais déjà que j'ai besoin d'améliorer l'impl pour supporter. Je suis ouvert sur ce que nous pourrions faire. La liaison à Debug pourrait être une bonne idée.

Hmm, c'est délicat. Je suppose que cela dépend si nous pensons qu'un programme "poli" utilisera ce trait impl. J'ai tendance à penser qu'il est correct de dire que "non, ils ne le feront pas" - en gros, un programme raffiné va soit (a) attraper la sortie et la gérer d'une autre manière ou (b) utiliser un nouveau type ou quelque chose qui implémente Debug le droit chemin. Cela signifierait que nous pouvons optimiser l'impl pour décharger des informations utiles mais nécessairement sous la forme la plus jolie (ce qui semble être le rôle de Debug ).

Ce pourrait être le bon choix de cibler très clairement le prototypage en utilisant Debug , car je ne pense pas que nous puissions jamais gérer automatiquement les erreurs d'une manière correcte pour la plupart des cas d'utilisation en production.

@sansbateaux Je suis d'accord.

@nikomatsakis Je suppose que vous vouliez dire " pas nécessairement sous la forme la plus jolie" ? Si oui, oui, je suis d'accord.

Mise à jour : après avoir travaillé dessus pendant quelques jours, j'ai basculé là-dessus. Voir ci-dessous.

:+1 : sur Debug , ici ; J'aime le "sorte d'analogue à une exception non interceptée" de @nikomatsakis de https://github.com/rust-lang/rfcs/pull/1937#issuecomment -284509933. Un commentaire de Diggsey suggérant également Debug : https://github.com/rust-lang/rfcs/pull/1937#issuecomment -289248751

Pour info, j'ai basculé sur le problème par défaut "plus complet" vs "plus convivial" (c'est-à-dire Debug vs Display trait lié).

Le TL; DR est que je pense maintenant que nous devrions définir la limite sur Display (selon le post original de @withoutboats ) pour fournir une sortie plus propre et récapitulative dans le cas « ne rien faire ».

Voici mon raisonnement :

Sur le problème RFC du trait termination , @zackw fait valoir que Rust a le double système panic / Result parce que panic s sont pour les bogues et Result s sont pour les erreurs. À partir de là, je pense qu'un cas convaincant peut être fait pour évaluer la présentation d'erreur par défaut indépendamment de la présentation de panique par défaut.

Bien sûr il n'y a pas un seul défaut qui satisfera tout le monde, alors en appliquant le principe de moindre surprise, je me demande quel défaut est le plus approprié ?

  • Une erreur n'est souvent pas gérée par conception , en ce sens qu'elle est destinée à être communiquée à l'utilisateur que quelque chose (éventuellement réparable) s'est mal passé (fichier introuvable, etc.). En tant que tel, le cas d'utilisation existe et pourrait raisonnablement être considéré comme courant que l'utilisateur soit le public visé.

  • Comme @nikomatsakis l' a souligné, quelle que soit la valeur par défaut que nous choisissons, tout développeur souhaitant modifier le comportement peut soit utiliser le modèle newtype, soit développer une implémentation personnalisée dans main().

Et enfin, du côté plus subjectif, en travaillant avec cette fonctionnalité au cours des deux derniers jours, j'ai trouvé que la sortie Debug m'a laissé l'impression que mon "application Rust" était moins polie :

$ foo
Error: Custom { kind: Other, error: StringError("returned Box<Error> from main()") }
$

vs

$ foo
Error: returned Box<Error> from main()
$

Le trait Dispay semble être une valeur par défaut beaucoup plus civilisée pour une erreur (par opposition à un bogue), n'est-ce pas ?

@ U007D attendez, laquelle de ces deux sorties préférez-vous ?

(a) Error: Custom { kind: Other, error: StringError("returned Box<Error> from main()") }

ou

(b) Error: returned Box<Error> from main()

Il préfère l'option (b).

@nikomatsakis À l'origine, j'étais d'accord avec a) Debug comme concept dans ma tête, mais après avoir travaillé avec pendant quelques jours en voyant le résultat, je préfère maintenant b) Display comme un défaut. Je pense que ma préférence pour b) deviendrait encore plus prononcée si je modélisais une erreur enchaînée.

Je ne pense pas que "poli" ou "civilisé" soit le but de cela, car j'ai compris que le fil avait déjà accepté cela comme étant principalement à titre d'exemple, les gens étant pleinement censés ajouter une gestion personnalisée à mesure que le programme mûrit.

Dans ces cas, pour moi, la "moins surprise" est une sortie orientée développeur, tout comme unwrap .

Cela vaudrait-il la peine de discuter {:#?} ici, si l'on craint une longue erreur ?

Le signalement d'erreurs pour les utilisateurs finaux sera différent pour chaque outil et chaque cas d'utilisation, mais le signalement d'erreurs pour les développeurs devrait ressembler à ce que Rust fait dans d'autres situations comme .unwrap() . Puisqu'il ne peut y avoir qu'une seule valeur par défaut et que le logiciel perfectionné devra de toute façon remplacer la sortie, je vote pour que la valeur par défaut soit orientée développeur avec Debug .

Je pense que le cœur de cette discussion est vraiment "qui est le public cible du message par défaut ?"

Disons un instant que nous étions tous d'accord pour dire que le public cible par défaut était les développeurs. Je pense que la limite par défaut Debug serait un choix simple.

Maintenant, disons un instant que nous avons convenu que le public cible par défaut était l'utilisateur, alors c'est là, respectivement, que je ne suis pas d'accord avec d'autres et que je pense que des qualités subjectives comme la sortie "polonaise" et "civilisée" ont une part importante à jouer. Pour certains, le "polissage" de la présentation de l'utilisateur final peut être la meilleure raison d'éviter Display . (Je ne partage pas ce point de vue, mais je le comprends et le respecte.)

Alors oui, je peux certainement voir des arguments raisonnables pour l'un ou l'autre groupe comme cible par défaut. Je pense que si un consensus fort se développe autour du public qui devrait être la cible par défaut, alors le choix du trait lié sera clair (euh)... :)

(Je ne connais pas tout ce sujet, mais) n'est-il pas concevable qu'il puisse y avoir de petits utilitaires pour lesquels la sortie d'erreur par défaut avec Termination serait parfaitement adéquate, à condition qu'elle soit dans un format présentable par l'utilisateur comme Display ? Dans ce cas, la seule raison pour laquelle les auteurs devraient recourir à la "gestion personnalisée" est si nous les fabriquons.

Quelqu'un peut-il fournir des exemples de ce à quoi ressemble la sortie dans chaque cas (je suppose que cela dépend également du type particulier E utilisé ?), Et quelles étapes les auteurs doivent réellement suivre s'ils veulent une "gestion personnalisée" au lieu? Je pars juste sur des hypothèses ci-dessus.

(La sortie ressemble-t-elle littéralement à ce que @U007D a collé ci-dessus ? Pourquoi imprimerait-elle "Returned Box\Boîte<Erreur> ?)

À quelle fréquence même le Display du message d'erreur est-il suffisamment convivial ? Par exemple, le programme suivant :

fn main() {
    if let Err(e) = std::fs::File::open("foo") {
        println!("{}", e)
    }
}

émet le message suivant :

No such file or directory (os error 2)

Ce qui, je dirais, n'est pas génial UX, surtout avec le nom de fichier non mentionné. Du moins pas à moins que le programme ne prenne littéralement un seul nom de fichier en entrée. D'un autre côté, ce n'est pas non plus une excellente expérience pour les développeurs , car il manque le fichier source/le numéro de ligne/la trace de la pile. La sortie Debug est, évidemment, une expérience utilisateur encore pire, et n'ajoute aucune information utile pour le décodeur non plus.

Donc, je suppose que ce que j'essaie de dire, c'est que sans améliorer le contenu informatif des erreurs de bibliothèque elles-mêmes, ni Debug ni Display ne sont formidables.

La sortie ressemble-t-elle littéralement à ce que @ U007D a collé ci-dessus ? Pourquoi imprimerait-il "boîte retournéefrom main()" au lieu de... le contenu réel de cette Box?

@glaebhoerl Vous avez raison - dans ce cas, le "contenu réel de ce Box<Error> " était un champ message personnalisé que j'avais créé pour tester le termination_trait , affiché textuellement . J'aurais pu écrire "foo bar baz" ou n'importe quoi d'autre à la place (mais cela n'aurait peut-être pas été aussi utile à un utilisateur exécutant les tests du compilateur).

En utilisant l'exemple de @jdahlstrom , voici la sortie réelle d'une bibliothèque standard Box ed "fichier introuvable" Error (notez, comme vous l'avez correctement souligné, aucune mention de boxe nulle part):
Debug :

$ foo
Error { repr: Os { code: 2, message: "No such file or directory" } }
$

et Display :

$ foo
No such file or directory (os error 2)
$

@jdahlstrom Je pense que vous faites valoir un bon point. Je suis d'accord que bien que les deux messages puissent mal desservir leur public cible et je tiens à souligner que fournir le mauvais message est encore pire (comme je pense que vous y avez fait allusion):

Fournir Display à un développeur a tous les inconvénients de Debug et manque la spécificité du type d'erreur qui est même affiché.

Fournir Debug à un utilisateur a tous les inconvénients de Display et ajoute encore plus d'informations techniques dont l'utilisateur n'a pas besoin et peut ne pas être en mesure de comprendre.

Alors oui, je suis d'accord que les messages ne sont souvent pas assez ciblés, pour l'un ou l'autre public. Je pense que cela met en évidence une autre raison importante pour nous d'être clairs sur qui nous ciblons afin que nous fournissions la meilleure expérience possible (malgré les défauts) pour ce groupe.

J'ai besoin d'aide pour implémenter le support de ? dans #[test] . Mon implémentation actuelle peut être trouvée ici : https://github.com/rust-lang/rust/compare/master...bkchr :termination_trait_in_tests

La compilation d'un test avec mes modifications entraîne l'erreur suivante :

error: use of unstable library feature 'test' (see issue #27812)
  |
  = help: add #![feature(test)] to the crate attributes to enable

@eddyb a dit que je ne devrais plus utiliser quote_item!/expr! , car ils sont hérités.
Que dois-je faire maintenant, passer à la nouvelle macro quote! ou tout retravailler dans la construction manuelle ast ?

J'apprécie toute aide :)

Je pense que générer une invocation de macro pour une macro définie dans libtest pourrait très bien fonctionner.

@eddyb Je ne suis pas sûr de comprendre votre suggestion ici:

Je pense que générer une invocation de macro à une macro définie dans libtest pourrait très bien fonctionner.

Oh, je suppose que oui. Vous dites - définir une macro dans libtest puis générer du code qui l'invoque ? Idée intéressante. Ce nom de macro ne serait-il pas une sorte de "fuite"? (c'est-à-dire qu'il devient une partie de l'interface publique de libtest ?)


@bkchr

La compilation d'un test avec mes modifications entraîne l'erreur suivante :

Avez-vous une idée de la raison pour laquelle cette erreur est générée ? Rien qu'en lisant le diff, je ne le sais pas, mais je peux essayer de construire localement et de le comprendre.

Je ne devrais plus utiliser quote_item!/expr! , car ils sont hérités.

Je n'ai pas d'opinion tranchée ici. Je suis d'accord avec @eddyb , ils sont hérités, mais je ne sais pas s'ils sont le genre d'héritage où l'ajout de quelques utilisations supplémentaires les rendra plus difficiles à supprimer - c'est-à-dire, une fois que nous aurons un remplacement récent, serait-il facile @eddyb pour passer de l'un à l'autre ?

La construction manuelle d'AST est certainement pénible, même si je suppose que nous avons des aides pour cela.

La plupart du temps, nous avons juste besoin de faire une petite modification, non ? c'est-à-dire, passer de l'invocation de la fonction au test du résultat de report() ?

PS, nous voulons probablement générer quelque chose comme Termination::report(...) plutôt que d'utiliser une notation .report() , pour éviter de compter sur le trait Termination dans la portée ?

Non, je ne sais pas d'où vient cette erreur :(

La RFC proposait de générer une fonction wrapper qui appelle la fonction de test d'origine. C'est aussi ma façon actuelle de faire.
Je pense que nous pourrions également supprimer la fonction wrapper, mais nous aurions alors besoin de boxer le pointeur de fonction, car chaque fonction de test peut renvoyer un type différent.

Hmm, comme le test importe déjà d'autres éléments, il n'est pas si compliqué d'importer également le trait Termination.

@alexcrichton avez-vous peut-être une idée d'où vient cette erreur ?

@nikomatsakis libtest est instable et ne pouvons-nous pas également marquer les macros comme instables même si ce n'était pas le cas ?

@eddyb oh, bon point.

La RFC proposait de générer une fonction wrapper qui appelle la fonction de test d'origine. C'est aussi ma façon actuelle de faire.

Une fonction wrapper me semble bien.

@eddyb avec la macro, vous voulez dire quelque chose comme create_test qui est défini dans libtest ? Mais ce que je ne comprends pas, c'est "marquer la macro comme instable". Qu'entendez-vous par là ? Pourriez-vous me donner un exemple?

@bkchr Mettre un attribut #[unstable(...)] sur la définition de la macro, par exemple : https://github.com/rust-lang/rust/blob/3a39b2aa5a68dd07aacab2106db3927f666a485a/src/libstd/thread/local.rs#L159 -L165

Donc, cette première case à cocher devrait-elle...

Implémenter la RFC

...à vérifier maintenant que le PR lié a été fusionné ?

@ErichDonGubler Terminé :)

Hmm, c'est actuellement seulement la moitié rfc qui est implémentée avec le pr fusionné ^^

Séparé en 3 cases à cocher :)

J'ai essayé d'utiliser cette fonctionnalité dans main et j'ai trouvé cela assez frustrant. Les implémentations existantes du trait de terminaison ne me permettent tout simplement pas "d'accumuler" facilement plusieurs types d'erreurs - par exemple, je ne peux pas utiliser failure::Fail , car il n'implémente pas Error ; Je ne peux pas utiliser Box<Error> , même raison. Je pense que nous devrions donner la priorité au passage à Debug . =)

Bonjour, @nikomatsakis ,

J'ai ressenti exactement la même frustration que vous lorsque j'ai essayé d'utiliser termination_trait .

Cela, combiné à vos messages sur le piratage du compilateur, m'a inspiré à résoudre ce problème plus tôt ce mois-ci. J'ai posté l'implémentation pour Display (et pour Debug dans le commit précédent) ainsi que des tests ici : https://github.com/rust-lang/rust/pull/47544. (C'est très mineur, mais quand même, mon premier PR de compilateur Rust ! :tada:) :)

Je crois comprendre que l'équipe lang fera un appel sur le trait à utiliser, mais dans tous les cas, la mise en œuvre est prête à démarrer.

Une question qui m'intéresse toujours: supposons que vous ne vouliez pas vous fier à la sortie du message d'erreur par défaut (que ce soit Debug ou Display ), et que vous vouliez la vôtre à la place, comment faites-vous ce? (Excuses si cela a déjà été écrit quelque part et que je l'ai raté.) Vous n'auriez pas à arrêter d'utiliser ? -in- main entièrement, n'est-ce pas ? C'est quelque chose comme écrire votre propre résultat et/ou type d'erreur et/ou impl ? (Cela me semble malheureux si ? -in- main n'était qu'un jouet, et dès que vous vouliez "devenir sérieux", vous devriez revenir à des méthodes moins ergonomiques.)

@glaebhoerl C'est très simple :

  1. Créez un nouveau type.
  2. Implémentez From votre ancien type d'erreur.
  3. Implémentez Debug (ou Display ).
  4. Remplacez le type dans la signature de main .

Merci!

Il me semble un peu étrange d'écrire des implémentations personnalisées de Debug qui ne sont pas orientées vers le débogage, mais je suppose que ce n'est pas la fin du monde.

@glaebhoerl C'est pourquoi l'implémentation Result devrait utiliser Display plutôt que Debug IMO.

Le trait Termination ne peut-il pas avoir une méthode supplémentaire appelée message , ou error_message ou quelque chose comme ça, qui a une implémentation par défaut qui utilise le Debug / Display pour afficher le message ? Ensuite, il vous suffit d'implémenter une seule méthode plutôt que de créer un nouveau type.

@glaebhoerl @Thomasdezeeuw Quelque chose comme une méthode error_message figurait dans le projet original de la RFC, mais elle a été abandonnée faute de consensus. Mon sentiment à l'époque était qu'il serait préférable d'obtenir la fonctionnalité de base (pas nécessairement stabilisée) puis de l'itérer.

@zackw D'accord, personnellement, je serais d'accord sans messagerie, juste un numéro, disons 0 et 1 pour les codes d'erreur. Mais si nous voulons obtenir la messagerie dans la première itération, je pense que je serais plus en faveur d'un Termination::message que de n'importe quoi sur Debug ou Display .

Cette méthode message renverrait-elle un String ? Cela ne serait-il pas incompatible avec le fait que Termination soit dans libcore ?

@SimonSapin Termination est actuellement défini dans libstd .

Hmm, mais je ne pense pas que l'ajout d'une méthode message au trait serait la meilleure implémentation. Que renverrait la méthode pour des types comme i32 ? Comment déciderait quand imprimer ce message ? Actuellement, l'implémentation de Termination pour Result imprime l'erreur dans la fonction report . Cela fonctionne, car Result sait qu'il s'agit d'un Err . Nous pourrions intégrer quelque part un chèque report() != 0 puis l'imprimer, mais cela ne semble pas correct.
Le problème suivant serait que nous voulons fournir une implémentation standard pour Result , mais que doit probablement implémenter le type Error pour être imprimé ? Cela nous ramènerait à la question actuelle Debug ou Display .

Un autre casse-tête qui survient si vous souhaitez utiliser ? dans main dans un programme "sérieux" est que, dans certaines circonstances, les programmes en ligne de commande veulent quitter avec un statut différent de zéro mais sans imprimer _rien_ (considérez grep -q ). Alors maintenant, vous avez besoin d'un Termination impl pour quelque chose qui _n'est pas_ un Error , qui n'imprime rien, qui vous permet de contrôler le statut de sortie ... et vous devez décider si oui ou non vous re retournant cette chose _après_ l'analyse des arguments de la ligne de commande.

C'est ce que je pense:

Le retour Result<T, E> doit utiliser l'implémentation de débogage pour E. C'est le trait le plus pratique - il est largement implémenté et satisfait le cas d'utilisation "sortie rapide et sale" ainsi que le cas d'utilisation des tests unitaires. Je préférerais ne pas utiliser Display à la fois parce qu'il est moins largement implémenté et parce qu'il donne l'impression qu'il s'agit d'une sortie soignée, ce qui, à mon avis, est hautement improbable.

Mais il devrait également y avoir un moyen d'obtenir une sortie d'aspect professionnel. Heureusement, il existe déjà deux méthodes de ce type. Tout d'abord, comme l'a dit @withoutboats , les utilisateurs peuvent créer un pont "affichage à partir du débogage" s'ils souhaitent utiliser E: Display ou donner une sortie de style professionnel. Mais si cela vous semble trop piraté, vous pouvez également simplement définir votre propre type pour remplacer le résultat. par exemple, vous pourriez faire :

fn main() -> ProfessionalLookingResult {
    ...
}

puis implémentez Try pour ProfessionalLookingResult . Ensuite, vous pouvez également implémenter Terminate , peu importe :

impl Terminate for ProfessionalLookingResult {
    fn report(self) -> i32 {
        ...
        eprintln!("Something very professional here.");
        return 1;
        ...
    }
}

Je suis d'accord avec @nikomatsakis que cela devrait utiliser Debug .

Je pense aussi que pour une sortie soignée, écrire du code dans main est probablement mieux que de créer un nouveau type pour implémenter Try et Terminate . Le point de Terminate me semble être pour les choses que les bibliothèques peuvent facilement faire une bonne valeur par défaut, ce qui n'est jamais le cas pour une situation dans laquelle la façon dont le programme se termine est importante pour les utilisateurs finaux (par exemple, les CLI professionnelles) .

Bien sûr, d'autres personnes peuvent avoir une opinion différente, et il existe plusieurs façons d'utiliser les traits impliqués pour injecter du code au lieu de l'écrire directement dans main . Ce qui est formidable, c'est que nous avons plusieurs choix et que nous n'avons pas toujours à trouver une seule façon bénie de gérer les erreurs.

Permettez-moi de noter quelques réflexions, bien qu'elles posent quelques problèmes.

J'aimerais voir la suite

fn main() -> i32 {
    1
}

Ce qui pourrait s'écrire plus généralement comme suit :

fn main() -> impl Display {
    1
}

Ces deux fonctions principales doivent renvoyer un code de sortie 0 et println! le Display de 1.

Cela devrait être aussi simple que ce qui suit (je pense).

impl<T> Termination for T where T: Display {
    fn report(self) -> i32 {
        println!("{}", self);
        EXIT_SUCCESS
    }
}

Alors pour les erreurs on peut avoir :

impl<T: Termination, E: Debug> Termination for Result<T, E> { ... }

où l'implémentation est la même que dans la RFC juste avec "{:?}" à utiliser
un format Debug .

Comme mentionné précédemment, les personnes qui ont besoin de plus de contrôle sur la sortie peuvent simplement
écrivez:

fn main() -> Result<i32, MyError> { ... }
impl Termination for Result<i32, MyError> { ... }

Bien que cela soit indécidable avec notre compilateur actuel, je suppose, car il
verrait des implémentations conflictuelles... Nous faisons donc ce que suggère @nikomatsakis
et écris:

fn main() -> MyResult { ... }
impl Termination for MyResult { ... }
or, if you want something more general.
impl<T, E> Termination for MyResult<T, E> { ... }

Je sais que cela reformule en partie des choses qui ont été dites, mais j'ai pensé que je présenterais ma vision dans son ensemble, montrant qu'une solution plus générale pour afficher les valeurs de retour, pas seulement les résultats. Il semble qu'une grande partie de ce commentaire discute des implémentations de Termination que nous expédions par défaut. De plus, ce commentaire est en contradiction avec une implémentation telle que impl Termination for bool telle que décrite dans la RFC. Personnellement, je pense que les codes de sortie non nuls doivent être gérés exclusivement par Results ou des types personnalisés qui implémentent Termination .

C'est une question intéressante de savoir comment gérer ensuite ? sur les types Option dans main puisqu'ils n'ont pas d'implémentation pour Display .

TL; DR : Je suis d'accord avec Debug .

Explication plus détaillée :
J'ai passé du temps à y réfléchir hier et aujourd'hui dans le but de découvrir les hypothèses implicites que j'ai ou que je fais pour aider à arriver à la meilleure réponse possible.

Hypothèse clé : de tous les différents types d'applications que l'on peut écrire dans Rust, je pense que l'application console est celle qui bénéficiera le plus / sera la plus impactée par cette décision. Ma pensée est que lors de l'écriture d'une bibliothèque, d'un titre de jeu AAA, d'un IDE ou d'un système de contrôle propriétaire, on ne s'attendrait probablement pas à ce qu'un trait de terminaison principal par défaut réponde à son besoin prêt à l'emploi (en effet, on pourrait même ne pas avoir un principale). Donc, mon biais vers Display par défaut vient de ce que nous nous attendons à voir lors de l'utilisation d'une petite application en ligne de commande, par exemple :

$ cd foo
bash: cd: foo: No such file or directory

La plupart d'entre nous ne s'attendent à aucune sorte d'aide au débogage, juste à un indicateur succinct de ce qui n'a pas fonctionné. Je plaide simplement pour cela comme une position par défaut.

Quand je pense écrire un Terminate impl pour obtenir une sortie simple comme celle-ci, je me rends compte que le ? de la fonctionnalité principale n'est pas si différent de la rouille stable aujourd'hui (en termes de quantité de code écrit ), où un "inner_main()" conscient Result est souvent créé afin de gérer E .

Avec cette hypothèse à l'esprit, en tant qu'exercice de réflexion, j'ai essayé de déterminer si j'étais convaincu que la prépondérance des implémentations de style " inner_main() " existantes aujourd'hui était d'une saveur plus décontractée Display (sur une saveur plus technique Debug ). Je pensais que ce serait une indication de la manière dont la fonctionnalité est susceptible d'être réellement utilisée.

Je n'ai pas pu me convaincre que ce soit le cas. (Ce qui signifie, je ne pense pas qu'il existe actuellement un fort biais vers Display dans les implémentations existantes). En effet, en parcourant mes propres référentiels que j'ai écrits au cours des 16 derniers mois, j'ai également trouvé suffisamment de cas dans les deux cas pour que je ne puisse pas dire que la mise en œuvre Display par défaut aurait représenté une économie nette.

Si l'on s'en tient à l'hypothèse "le principal bénéficiaire est l'application cli", il existe un grand nombre d'applications de console qui fournissent de l'aide et des informations d'utilisation. Par exemple:

$ git foo
git: 'foo' is not a git command. See 'git --help'.

The most similar command is
    log

Donc, même pour les applications de console, il m'est difficile d'identifier un "groupe blessé" en utilisant Debug .

Et enfin, je serais plus heureux avec un Debug impl qu'avec la fonctionnalité retenue pendant encore 6 mois, donc, égoïstement, il y a ça :).

Donc, il y a mon processus de pensée exposé en public. Pour résumer, je pense que Debug ne sera ni meilleur ni pire que Display , et en tant que tel, devrait convenir comme implémentation par défaut.

Comme beaucoup d'entre vous, j'en suis sûr, j'aurais aimé qu'il y ait une implémentation qui me passionne davantage - comme "OUI, C'EST ÇA !!!", TBH. Mais c'est peut-être juste que mes attentes sont irréalistes... Peut-être qu'une fois que nous aurons une solution qui fonctionnera avec failure réduisant le passe-partout dans mes projets, ça grandira sur moi. :)

Notez que j'ai ouvert un PR pour prendre en charge le trait terminatio dans les tests : #48143 (en s'appuyant sur le travail de @bkchr ).

J'ai pris la liberté d'étendre le trait Termination avec une méthode de traitement du résultat du test. Cela a simplifié la mise en œuvre, mais cela a également du sens, car les échecs de test peuvent nécessiter une sortie plus détaillée que les échecs exécutables.

Termination devrait être renommé Terminate après notre préférence générale pour les verbes pour les traits dans libstd.

@withoutboats Je pense qu'à un moment donné, il y a eu une discussion sur le fait que les traits verbaux sont principalement ceux qui ont une seule méthode avec le même nom que le trait. Quoi qu'il en soit, puis-je relancer ma propre suggestion de remise à vélos, Exit ?

Bikeshedding gratuit : il s'agit d'un trait à méthode unique. Si nous voulons leur donner le même nom, peut-être ToExitCode / to_exit_code ?

La stabilisation du retour Result peut être effectuée indépendamment de la stabilisation du trait, n'est-ce pas ?

Pour moi, cela ressemble beaucoup à ? où la plupart de la valeur provient de la fonctionnalité de langue, et nous pouvons retarder la détermination du trait. La discussion RFC m'a même fait me demander si le trait _ever_ doit être stabilisé, car mettre le code dans un inner_main semblait plus facile qu'un trait impl pour cela ...

Oui, nous n'avons pas besoin de stabiliser le trait - bien qu'il soit utile sur stable pour des choses comme les frameworks, qui ne peuvent pas nécessairement compter autant sur inner_main .

@SimonSapin Je pense que To fait référence à une conversion de type, et ce n'est pas le cas. Mais nous pourrions nommer la méthode terminate (je ne pense pas non plus que cette restriction sur le moment de nommer les verbes de traits soit valable. Try est un contre-exemple évident.)

J'ai proposé de stabiliser fn main() -> TT n'est pas l'unité. Cela laisse de nombreux détails - en particulier le nom/l'emplacement/les détails du trait - non stabilisés, mais certaines choses sont corrigées. Détails ici :

https://github.com/rust-lang/rust/issues/48453

Merci de donner votre avis !

terminate semble être plus descriptif que report . Nous avons toujours terminate , mais nous pouvons omettre report ing.

Mais contrairement à par exemple std::process::exit cette méthode ne termine rien. Il convertit uniquement la valeur de retour de main() en un code de sortie (après avoir éventuellement imprimé Result::Err sur stderr).

Un autre vote pour Exit . J'aime le fait qu'il soit bref, assez descriptif et cohérent avec le concept traditionnel de codes de sortie/statut de sortie.

Je préférerais certainement Exit à Terminate puisque nous quittons gracieusement en revenant du principal, plutôt que de nous arrêter soudainement là où nous sommes parce que quelque chose s'est vraiment mal passé.

J'ai ajouté https://github.com/rust-lang/rust/issues/48854 pour proposer des tests unitaires de stabilisation qui renvoient des résultats.

Oh hé, j'ai trouvé le bon endroit pour en parler.

Utilisation ? dans les doctests

La façon dont les doctests fonctionnent actuellement ressemble à ceci :

  • rustdoc analyse le doctest pour savoir s'il déclare un fn main

    • (actuellement, il ne fait qu'une recherche de texte ligne par ligne pour "fn main" qui n'est pas après un // )

  • Si fn main a été trouvé, il ne touchera pas ce qui est déjà là
  • Si fn main n'a pas été trouvé, il enveloppera la plupart* du doctest dans un fn main() { } base

    • * Version complète : elle extraira les déclarations #![inner_attributes] et extern crate et les placera en dehors de la fonction principale générée, mais tout le reste ira à l'intérieur.

  • Si le doctest n'inclut aucune instruction de caisse externe (et que la caisse documentée ne s'appelle pas std ), alors rustdoc insérera également une instruction extern crate my_crate; juste avant la fonction principale générée.
  • rustdoc compile et exécute ensuite le résultat final en tant que binaire autonome, dans le cadre du harnais de test.

(J'ai omis quelques détails, mais j'ai commodément fait un article complet ici.)

Donc, pour utiliser ? manière transparente dans un doctest, le bit qui doit changer est la partie où il ajoute fn main() { your_code_here(); } Déclarer votre propre fn main() -> Result<(), Error> fonctionnera dès que vous pourrez le faire que dans le code normal - rustdoc n'a même pas besoin d'y être modifié. Cependant, pour le faire fonctionner sans déclarer main manuellement, il faudra un petit ajustement. N'ayant pas suivi de près cette fonctionnalité, je ne sais pas s'il existe une solution unique. Est-ce fn main() -> impl Termination est possible ?

Est-ce que fn main() -> impl Termination est possible?

Dans un sens superficiel, oui : https://play.rust-lang.org/?gist=8e353379f77a546d152c9113414a88f7&version=nightly

Malheureusement, je pense que -> impl Trait est fondamentalement gênant avec ? en raison de la conversion d'erreur intégrée, qui a besoin du contexte d'inférence pour lui dire quel type utiliser : https://play.rust- lang.org/?gist=23410fa4fa684710bc75e16f0714ec4b&version=nightly

Personnellement, j'imaginais ? -in-doctests fonctionnant via quelque chose comme https://github.com/rust-lang/rfcs/pull/2107 comme fn main() -> Result<(), Box<Debug>> catch { your_code_here(); } (en utilisant la syntaxe de https:// github.com/rust-lang/rust/issues/41414#issuecomment-373985777).

La version impl Trait est cool, cependant, et pourrait fonctionner s'il y avait une sorte de support "préférer le même type si sans contrainte" que rustc pourrait utiliser en interne dans le désugar ? , mais mon souvenir est que l'idée d'une telle fonctionnalité a tendance à faire reculer d'horreur les personnes qui comprennent comment les choses fonctionnent :sweat_smile: Mais peut-être que c'est une chose interne qui ne s'applique que si la sortie est impl Trait serait faisable...

Oh, ce sont des préoccupations très réelles. J'avais oublié comment cela gâcherait l'inférence de type. Si le bloc de capture a commencé à s'enrouler correctement comme ce problème lié, cela semble être une voie beaucoup plus facile (et beaucoup plus rapide, en termes de stabilisation).

La seule chose que je me demande, c'est comment cela sera affecté par la transition d'édition. La syntaxe catch n'est-elle pas en train de changer à l'époque 2018 ? Rustdoc voudra probablement compiler les doctests dans la même édition que la bibliothèque qu'il exécute, il devra donc différencier les syntaxes en fonction de l'indicateur d'époque qui lui a été attribué.

Je crains que cela ne soit maintenant stabilisé mais des cas simples apparemment toujours ICE: https://github.com/rust-lang/rust/issues/48890#issuecomment -375952342

fn main() -> Result<(), &'static str> {
    Err("An error message for you")
}
assertion failed: !substs.has_erasable_regions(), librustc_trans_utils/symbol_names.rs:169:9

https://play.rust-lang.org/?gist=fe6ae28c67e7d3195a3731839d4aac84&version=nightly

A quel moment dit-on "c'est trop buggé et il faut déstabiliser" ? Il semble que si cela atteignait le canal stable dans sa forme actuelle, cela causerait beaucoup de confusion.

@frewsxcv Je pense que les problèmes sont résolus maintenant, n'est-ce pas ?

@nikomatsakis le problème que j'ai soulevé dans https://github.com/rust-lang/rust/issues/48389 est résolu dans 1.26-beta, donc oui de mon point de vue.

Oui, l'ICE dont je m'inquiétais est maintenant réparé !

Toutes mes excuses pour intervenir à un moment où il est probablement trop tard pour faire quoi que ce soit à ce sujet, mais je voulais laisser mes commentaires ici au cas où il y en aurait. J'ai lu la majeure partie de ce fil, donc je parle avec ce contexte à l'esprit. Cependant, ce fil est long, donc s'il semble que j'ai oublié quelque chose, alors je l'ai probablement fait et j'apprécierais qu'il me le signale. :-)

TL; DR - Je pense que montrer le message Debug d'une erreur était une erreur, et qu'un meilleur choix serait d'utiliser le message Display d'une erreur.

Au cœur de ma conviction est que, en tant que personne qui _construit régulièrement des programmes CLI dans Rust_, je ne me souviens pas m'être jamais beaucoup soucié de ce qu'est le message Debug d'un Error . À savoir, le Debug d'une erreur est, par conception, pour les développeurs, pas pour les utilisateurs finaux. Lorsque vous construisez un programme CLI, son interface est fondamentalement destinée à être lue par les utilisateurs finaux, donc un message Debug a très peu d'utilité ici. Autrement dit, si un programme CLI que j'ai envoyé aux utilisateurs finaux affichait la représentation de débogage d'une valeur Rust en fonctionnement normal, je considérerais qu'il s'agit d'un bogue à corriger. Je pense généralement que cela devrait être vrai pour tous les programmes CLI écrits en Rust, bien que je comprenne que cela puisse être un point sur lequel des personnes raisonnables peuvent être en désaccord. Cela dit, une implication quelque peu surprenante de mon avis est que nous avons effectivement stabilisé une fonctionnalité où son mode de fonctionnement par défaut vous lance avec un bogue (encore une fois, IMO) qui devrait être corrigé.

En montrant la représentation Debug d'une erreur par défaut, je pense aussi que nous encourageons les mauvaises pratiques. En particulier, il est très courant au cours de l'écriture d'un programme Rust CLI d'observer que même l' Display d'une erreur n'est pas assez bonne pour être consommée par les utilisateurs finaux, et qu'un véritable travail doit être fait pour répare le. Un exemple concret de ceci est io::Error . Afficher un io::Error sans chemin de fichier correspondant (en supposant qu'il provienne de la lecture/écriture/ouverture/création d'un fichier) est fondamentalement un bogue, car il est difficile pour un utilisateur final de faire quoi que ce soit avec. En choisissant d'afficher la Debug d'une erreur par défaut, nous avons rendu plus difficile la découverte de ce type de bogues par les créateurs de programmes CLI. (En plus de cela, le Debug d'un io::Error est beaucoup moins utile que son Display , mais cela en soi n'est pas un gros problème d'après mon expérience. )

Enfin, pour compléter mon propos, j'ai aussi du mal à imaginer les circonstances dans lesquelles j'utiliserais ?-in-main même dans des exemples. À savoir, j'ai essayé d'écrire des exemples qui correspondent le plus possible aux programmes du monde réel, et cela a généralement impliqué d'écrire des choses comme ceci :

use std::error::Error;
use std::process;

fn try_main() -> Result<(), Box<Error>> {
    // do stuff with `?`
}

fn main() {
    if let Err(err) = try_main() {
        eprintln!("{}", err);
        process::exit(1);
    }
}

À première vue, ce serait _charmant_ de le remplacer par ?-in-main , mais je ne peux pas, car il n'affichera pas les Display d'une erreur. Autrement dit, lors de l'écriture d'un vrai programme CLI, j'utiliserai en fait l'approche ci-dessus, donc si je veux que mes exemples reflètent la réalité, alors je pense que je devrais montrer ce que je fais dans de vrais programmes et ne pas prendre de raccourcis (dans une mesure raisonnable ). Je pense en fait que ce genre de chose est vraiment important, et un effet secondaire de cela historiquement était qu'il montrait aux gens comment écrire du code Rust idiomatique sans saupoudrer unwrap partout. Mais si je reviens à l'utilisation ?-in-main dans mes exemples, alors j'ai juste renié mon objectif : j'installe maintenant des gens qui ne savent peut-être pas mieux écrire des programmes qui, par défaut, émettent très messages d'erreur inutiles.

Le modèle "émettre un message d'erreur et quitter avec un code d'erreur approprié" est en fait utilisé dans les programmes raffinés. Par exemple, si ?-in-main utilisait Display , alors je pourrais fusionner les fonctions main et run dans ripgrep aujourd'hui :

https://github.com/BurntSushi/ripgrep/blob/64317bda9f497d66bbeffa71ae6328601167a5bd/src/main.rs#L56 -L86

Bien sûr, je pourrais utiliser ?-in-main à l'avenir en fournissant mon propre impl pour le trait Termination une fois que cela se stabilise, mais pourquoi devrais-je prendre la peine de le faire si je pouvais simplement écrire le main fonction impl dans les exemples afin qu'ils correspondent à la réalité, et à ce stade, je pourrais aussi bien m'en tenir aux exemples que j'ai aujourd'hui (en utilisant un main et un try_main ).

À première vue, il semble que résoudre ce problème serait un changement radical. C'est-à-dire que ce code compile aujourd'hui sur Rust stable :

#[derive(Debug)]
struct OnlyDebug;

fn main() -> Result<(), OnlyDebug> {
    Err(OnlyDebug)
}

Je pense que passer à Display casserait ce code. Mais je n'en suis pas sûr ! S'il s'agit vraiment d'un problème de bateau a navigué, alors je comprends et il n'est pas trop utile d'insister sur ce point, mais je suis assez convaincu à ce sujet pour au moins dire quelque chose et voir si je ne peux pas convaincre les autres et voir s'il y a quoi que ce soit qui puisse être fait pour y remédier. (Il est également tout à fait possible que je réagisse de manière excessive ici, mais jusqu'à présent, quelques personnes m'ont demandé "pourquoi n'utilisez-vous pas ?-in-main ?" dans mes exemples CSV, et ma réponse a été essentiellement, "Je ne vois pas comment il serait possible de faire cela." Peut-être que ce n'était pas un problème qui devait être résolu par ?-in-main , mais certaines personnes ont certainement eu cette impression. Avec son implémentation actuelle , je pourrais le voir utile dans les tests doc et les tests unitaires, mais j'ai du mal à penser à d'autres situations dans lesquelles je l'utiliserais.)

Je suis d'accord que l'affichage du Display sur le Debug est préférable pour les applications CLI. Je ne suis pas d'accord avec le fait qu'il devrait être Display au lieu de Debug , car cela limite considérablement les erreurs qui peuvent réellement être ? -ed et va à l'encontre de l'objectif de ?-in-main . Autant que je sache (cela pourrait être totalement faux car je n'ai pas le temps de compiler stdlib et je ne sais pas si la spécialisation couvre cela), il n'y a aucune raison pour que nous ne puissions pas ajouter l'implémentation suivante à Termination. Cela fournirait un moyen ininterrompu d'utiliser Display lorsqu'il est disponible et de revenir à Debug lorsqu'il ne l'est pas.

#[unstable(feature = "termination_trait_lib", issue = "43301")]
impl<E: fmt::Display> Termination for Result<!, E> {
    fn report(self) -> i32 {
        let Err(err) = self;
        eprintln!("Error: {}", err);
        ExitCode::FAILURE.report()
    }
}

Enfin, pour compléter mon propos, j'ai aussi du mal à imaginer les circonstances dans lesquelles j'utiliserais ?-in-main même dans des exemples.

Je suis d'accord avec @BurntSushi en général pour les vrais programmes CLI, mais pour les scripts aléatoires et les outils internes que je prévois d'utiliser, c'est vraiment pratique. De plus, c'est vraiment pratique pour les prototypes et les jouets. Nous pourrions toujours décourager son utilisation dans le code de production, n'est-ce pas ?

Une partie de l'objectif avec ?-in-main est d'éviter le code unidiomatique dans les exemples de code, à savoir les déballages. Mais si ?-in-main lui-même devient unidiomatique, alors cela mine toute la fonctionnalité. Je voudrais fortement éviter tout résultat qui nous obligerait à décourager son utilisation, dans le code de production ou autrement.

J'ai essayé d'utiliser cette fonctionnalité dans main et j'ai trouvé cela assez frustrant. Les implémentations existantes du trait de terminaison ne me permettent tout simplement pas "d'accumuler" commodément plusieurs types d'erreurs -- par exemple, je ne peux pas utiliser failure::Fail, car il n'implémente pas Error ; Je ne peux pas utiliser Box, même raison. Je pense que nous devrions donner la priorité au passage au débogage. =)

Si nous utilisons Display , nous pouvons introduire un type rapide et sale comme MainError pour accumuler plusieurs types d'erreurs :

pub struct MainError {
    s: String,
}

impl std::fmt::Display for MainError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        self.s.fmt(f)
    }
}

impl<T> From<T> for MainError where T: std::error::Error {
    fn from(t: T) -> Self {
        MainError {
            s: t.to_string(),
        }
    }
}

Cela permettra quelque chose comme ci-dessous similaire à Box<Error> :

fn main() -> Result<(), MainError> {
    let _ = std::fs::File::open("foo")?;
    Ok(())
}

La discussion précédente sur Display vs Debug se trouve dans la partie cachée ici, commençant autour de https://github.com/rust-lang/rust/issues/43301#issuecomment -362020946.

@BurntSushi Je suis d'accord avec vous. Si cela ne peut pas être résolu, il pourrait y avoir une solution de contournement :

use std::fmt;
struct DisplayAsDebug<T: fmt::Display>(pub T);

impl<T: fmt::Display> Debug for DisplayAsDebug {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        fmt::Display::fmt(&self.0, f)
    }
}

impl<T: fmt::Display> From<T> for DisplayAsDebug {
    fn from(val: T) -> Self {
        DisplayAsDebug(val)
    }
}

Si c'était dans std::fmt , nous pourrions utiliser quelque chose comme ceci :

use std::{fmt, io};

fn main() -> Result<(), fmt::DisplayAsDebug<io::Error>> {
    let mut file = File::open("/some/file")?;
    // do something with file
}

Je n'ai toujours pas le temps de contribuer davantage à cela dans un avenir prévisible, mais je suis également d'accord que ? dans main devrait être utilisable dans des programmes CLI sérieux, et il y avait des fonctionnalités dans le brouillon original du RFC qui étaient destinés à faciliter cela. Ils ont été abandonnés au nom de l'incrémentalisme, mais il faudrait peut-être les revoir.

Je pense que c'est bien de résoudre ce problème d'une manière rétrocompatible si c'est fait bientôt, avant que beaucoup de code ne l'utilise sur stable.

En tant que personne qui écrit beaucoup de programmes CLI dans Rust et qui est responsable du style Rust au travail, je suis assez d'accord avec @burntsushi ici. J'aurais volontiers utilisé la version de cette fonctionnalité spécifiée dans la RFC.

Mais je considère que montrer une implémentation Debug à un utilisateur est un bogue, et pas beaucoup mieux que d'appeler simplement unwrap partout dans un programme CLI. Donc, même dans un exemple de code, je ne peux pas imaginer utiliser cette version de la fonctionnalité. C'est dommage, car la suppression du passe-partout de main aurait simplifié l'apprentissage des nouveaux développeurs Rust au travail, et nous n'aurions plus eu besoin d'expliquer quick_main! ou l'équivalent.

À peu près les seules circonstances où je peux imaginer l'utiliser seraient d'éliminer unwrap des doctests. Mais je ne sais pas si c'est encore supporté.

Si nous utilisons Display , nous pouvons introduire un type rapide et sale comme MainError pour accumuler plusieurs types d'erreurs :

Personnellement, ce type de solution de contournement ajouterait suffisamment de complexité pour qu'il soit simplement plus facile d'éviter ? -in- main complètement.

@Aaronepower :

Autant que je sache (cela pourrait être totalement faux car je n'ai pas le temps de compiler stdlib et je ne sais pas si la spécialisation couvre cela), il n'y a aucune raison pour que nous ne puissions pas ajouter l'implémentation suivante à Termination. Cela fournirait un moyen ininterrompu d'utiliser Display lorsqu'il est disponible et de revenir à Debug lorsqu'il ne l'est pas.

Si cela me permettait d'écrire fn main() -> Result<(), failure::Error> et d'obtenir une belle erreur lisible par l'homme en utilisant Display , cela satisferait certainement mes principales préoccupations. Cependant, je suppose que cela laisserait toujours la question d'afficher éventuellement le backtrace dans failure::Error lorsque RUST_BACKTRACE=1 est défini, mais cela pourrait être hors de portée, de toute façon, ou du moins un problème qui devrait être pris avec failure .

Cela s'est stabilisé il y a quelque temps , et je ne pense pas que nous puissions vraiment changer le comportement de Result . Personnellement, je reste assez satisfait du comportement actuel pour les cas d'utilisation que j'ai tendance à faire - des scripts rapides et sales, etc. - mais je conviens que les scripts plus complexes n'en voudront pas. Cependant, au moment de la discussion, nous avons également discuté d'un certain nombre de façons de contrôler ce comportement (je n'arrive pas à trouver ces commentaires maintenant car github me cache des choses).

Par exemple, vous pouvez définir votre propre "wrapper" pour le type d'erreur et implémenter From pour celui-ci :

struct PrettyPrintedError { ... }
impl<E: Display> From<E> for PrettyPrintedError { }

impl Debug { /* .. invoke Display .. */ }

Maintenant, vous pouvez écrire quelque chose comme ceci, ce qui signifie que vous pouvez utiliser ? dans main :

fn main() -> Result<(), PrettyPrintedError> { ... }

Peut-être qu'un tel type devrait faire partie de quick-cli ou quelque chose comme ça ?

@nikomatsakis Ouais, je comprends totalement cette solution de contournement, mais j'ai l'impression que cela va à l'encontre du but de la concision d'utiliser ?-in-main . J'ai l'impression que "il est possible d'utiliser ?-in-main en utilisant cette solution de contournement" mine malheureusement ?-in-main lui-même. Par exemple, je ne vais pas écrire votre solution de contournement dans des exemples succincts et je ne vais pas non plus imposer une dépendance à quicli pour chaque exemple que j'écris. Je ne vais certainement pas l'utiliser non plus pour des programmes rapides, car je veux la sortie Display d'une erreur dans pratiquement tous les programmes CLI que j'écris. Mon opinion est que la sortie de débogage d'une erreur est un bogue dans les programmes CLI que vous mettez devant les utilisateurs.

L'alternative à ?-in-main est une fonction supplémentaire de ~4 lignes. Donc, si la solution de contournement pour fixer ?-in-main à utiliser Display est bien plus que cela, alors personnellement, je ne vois pas vraiment de raison de l'utiliser (en dehors des tests de documentation ou des tests unitaires, comme mentionné ci-dessus).

Est-ce quelque chose qui pourrait être modifié dans l'édition ?

@BurntSushi Que penseriez-vous de la solution de contournement dans std , de sorte que vous n'auriez qu'à écrire une déclaration de type de retour un peu plus longue sur main() ?

@Kixunil Mon sentiment initial est que cela pourrait être acceptable. Je n'y ai pas trop réfléchi cependant.

@BurntSushi

L'alternative à ?-in-main est une fonction supplémentaire de ~4 lignes.

En fin de compte, le comportement est stable et je ne pense pas que nous puissions le modifier de manière réaliste à ce stade. (Je veux dire que ce n'est pas une violation de la solidité ou quelque chose.)

Cela dit, je trouve toujours la configuration actuelle assez agréable et préférable à Display , mais je suppose que c'est une question de ce que vous voulez que la "valeur par défaut" soit - c'est-à-dire que l'une ou l'autre configuration peut fonctionner comme la autre via divers nouveaux types. Donc, soit nous privilégions par défaut les scripts "quick-n-dirty", qui veulent Debug , soit des produits finis raffinés. J'ai tendance à penser qu'un produit final raffiné est celui où je peux me permettre une importation supplémentaire assez facilement, et aussi où j'aimerais choisir parmi de nombreux formats différents possibles (par exemple, est-ce que je veux juste l'erreur, ou est-ce que je veux inclure argv [0], etc.).

Pour être plus concret, j'imagine que ça ressemblerait à ça :

use failure::format::JustError;

fn main() -> Result<(), JustError> { .. }

ou, pour obtenir un format différent, peut-être que je fais ceci:

use failure::format::ProgramNameAndError;

fn main() -> Result<(), ProgramNameAndError> { .. }

Ces deux éléments sont raisonnablement agréables par rapport à la fonction à 4 lignes que vous deviez écrire auparavant :

use std::sys;

fn main() {
  match inner_main() {
    Ok(()) => { }
    Err(error) => {
      println!("{}", error);
      sys::exit(1);
    }
}

fn inner_main() -> Result<(), Error> {
  ...
}

Je reviens sur ce sujet car, pour la qualité de la production, ajouter ce qui équivaut essentiellement à un type d'erreur personnalisé pour la sortie semble être une bonne direction pour pousser les gens de toute façon. Cela semble parallèle à la façon dont panic! , par exemple, donne des messages de qualité de débogage par défaut.

@nikomatsakis C'est peut-être une meilleure vision à long terme, et je trouve votre résultat final appétissant. Ce serait bien si ces types d'erreurs se retrouvaient un jour dans std afin qu'ils puissent être utilisés dans des exemples avec le moins de surcharge possible. :-)

Remarque sur le processus : j'ai essayé d'encadrer mes commentaires initiaux ici avec "y a-t-il quelque chose qui peut être fait" plutôt que "changeons quelque chose qui s'est déjà stabilisé", dans un effort pour me concentrer sur l'objectif de haut niveau plutôt que de demander quelque chose que nous certainement pas faire. :-)

Et bien ça n'a pas pris longtemps : https://crates.io/crates/exitfailure 😆

Je me demande combien de personnes sont surprises en utilisant le trait Debug par rapport au trait Display . Le brouillon de discussion initial et le RFC final utilisent Display . Ceci est similaire à l'incident de trait d'implémentation universel, où les gens pensaient qu'ils obtenaient une chose, mais ne savaient qu'ils en obtenaient une autre après qu'elle se soit stabilisée. Considérant également que de nombreuses personnes surprises n'utiliseront pas cette fonctionnalité, je pense qu'il n'y aura pas de grosse barre oblique inversée si nous la modifions dans la prochaine édition.

@WiSaGaN Les détails d'un RFC peuvent et vont changer de temps à autre. Ceci est attendu et sain. Les RFC sont des documents de conception et non une représentation parfaite de ce qui est et sera toujours. La découverte d'informations est difficile, et c'est de ma faute si je n'y prête pas plus attention. J'espère que nous pourrons éviter de remettre en question la stabilité de cette fonctionnalité et nous concentrer plutôt sur les objectifs de niveau supérieur sur lesquels nous pouvons agir de manière réaliste.

@nikomatsakis Le principal problème que je vois en favorisant les cas rapides et sales est qu'il existe déjà un moyen de les résoudre: .unwrap() . Donc, de ce point de vue, ? n'est qu'une autre façon d'écrire des choses rapides et sales, mais il n'y a pas de façon aussi simple d'écrire des choses raffinées.

Bien sûr, le navire a navigué, donc nous sommes pour la plupart foutus. J'aimerais voir cela changé dans la prochaine édition, si possible.

@Kixunil , unwrap n'est vraiment pas une bonne façon d'aborder les choses, car le titre de ce fil indique que nous aimerions pouvoir utiliser le sucre syntaxique ? dans main . Je peux comprendre d'où vous venez (en fait, cela faisait partie de mes hésitations initiales avec l'utilisation de ? pour la gestion des options) mais avec l'inclusion de ? dans la langue, ce problème est un jolie convivialité gagner IMHO. Si vous avez besoin d'une sortie plus agréable, il existe des options pour vous. Vous ne livrez pas les choses aux utilisateurs sans les tester au préalable, et la découverte que vous avez besoin d'un type personnalisé pour la sortie de main sera une réalisation assez rapide.

En ce qui concerne "pourquoi" nous aimerions ? en main , réfléchissez à quel point c'est bizarre maintenant. Vous pouvez l'utiliser pratiquement partout ailleurs (puisque vous avez le contrôle du type de retour). La fonction main finit alors par se sentir un peu spéciale, ce qu'elle ne devrait pas.

.unwrap() est une solution simple et rapide qui ne compose pas, vous ne pouvez donc pas prendre en compte le code dans et hors de main() pendant que vous esquissez un programme.

En comparaison, ? est une solution rapide et sale qui compose, et la rendre non rapide et sale consiste à mettre le type de retour correct sur main() , pas de modifier le code lui-même.

@Screwtapello ah, maintenant ça a du sens pour moi. Merci!

Je veux juste exprimer que je pensais que le but était d'apporter ? dans main pour toutes les applications sans avoir à recourir à des wrappers supplémentaires... Si c'est juste pour tester je ne le fais pas y voient beaucoup d'avantages et continueront de s'en tenir à .unwrap() , malheureusement.

@oblitum Ce n'est pas seulement pour tester. Il n'utilisera tout simplement pas (par défaut) le format Display . Cela peut ou non être la sortie que vous recherchez, mais sera presque certainement meilleure que la sortie de .unwrap . Cela dit, cela pourrait rendre votre code "plus agréable" d'utiliser ? . Dans tous les cas, il est toujours possible de personnaliser la sortie des erreurs ? dans main comme @nikomatsakis l'a détaillé ci-dessus.

Serait-il possible d'introduire un nouveau type dans le stdlib pour la vitrine ? Quelque chose comme:

#[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)]
#[must_use]
struct DisplayResult<E: fmt::Display>(Result<(), E>)

impl<E> Termination for DisplayResult<E> {
    // ... same as Result, but use "{}" instead of "{:?}"
}

impl<E> From<Result<(), E>> for DisplayResult<E> {}
impl<E> Deref<Result<(), E>> for DisplayResult<E> {}
impl<E> Try for DisplayResult<E> {}
// ...

Cela devrait permettre aux utilisateurs d'écrire :

fn main() -> DisplayResult<MyError> {
    // Ordinary code; conversions happen automatically via From, Try, etc.
}

Les principales motivations ici sont :

  • L'affichage est utilisé lorsqu'une erreur est renvoyée
  • Les utilisateurs n'ont pas à faire d'emballage supplémentaire de leurs types d'erreur ou de résultat ; ils peuvent simplement utiliser ? dans leur fonction principale.

Suivi : il me semble que la sémantique de ? et From peut ne pas permettre à cela de fonctionner aussi implicitement que je le souhaiterais. Je sais que ? convertira entre les types d'erreur via From , mais je ne sais pas si cela fait la même chose pour différents implémenteurs de Try . Autrement dit, il convertira de Result<?, io::Error> en Result<?, FromIoError> , mais pas nécessairement de Result<?, Err> en DisplayResult<Result<?, Err>> . En gros, je cherche quelque chose comme ça pour fonctionner:

fn main() -> DisplayResult<io::Error> {
    let f = io::File::open("path_to_file")?;
    let result = String::new();
    f.read_to_string(result)?
}

En supposant que cela ne fonctionne pas, peut-être pourrait-on en faire un simple indicateur de compilation conditionnelle dans Cargo.toml ?

@Lucretiel : vous semblez supposer que les programmeurs veulent principalement imprimer un message de sortie sur la console. Cela ne vaut pas pour les applications de bureau non techniques, les démons avec plomberie de journalisation, etc.

Je suppose que les programmeurs qui renvoient un Result<(), E> where E: Error partir de main sont intéressés par l'impression d'un message d'erreur sur la console, oui. Même le comportement actuel, qui est print via Debug , est "imprimer un message de sortie sur la console".

Il semble y avoir un large accord dans ce fil sur le fait que quelque chose doit être imprimé pour stderr; la décision clé à prendre est "Devrait-il imprimer avec "{}" ou "{:?}" , ou autre chose, et dans quelle mesure devrait-il être configurable, le cas échéant".

@Lucretiel , pour moi, la valeur de retourner un Result contrôle bien le statut de sortie. Est-ce que l'impression de quelque chose de pas mieux serait laissée au gestionnaire de panique ? Il n'y a pas seulement la question de savoir s'il faut imprimer quelque chose sur stderr, mais aussi ce qu'il faut imprimer (backtrace, message d'erreur, etc.?). Voir https://github.com/rust-lang/rust/issues/43301#issuecomment -389099936, en particulier. la dernière partie.

Je l'ai utilisé récemment et j'aurais aimé qu'il s'appelle Display . Je suis sûr que nous avons fait le mauvais appel ici.

Un échec de processus que je remarque est que la décision a été prise par l'équipe lang (principalement Niko et moi) plutôt que par l'équipe libs, mais le code en question vit entièrement dans std. Libs aurait peut-être pris une meilleure décision.

@sansbateaux est-il trop tard pour changer ? Je remarque que la fonctionnalité est toujours étiquetée nocturne uniquement/expérimentale dans la documentation , mais il est possible que je ne comprenne pas une partie de la granularité derrière le processus de stabilisation.

Moi aussi, j'aimerais voir ce changement et je regrette de ne pas avoir été un défenseur plus fort lorsque l'équipe m'a demandé de changer l'implémentation de Display à Debug .

Le trait @Lucretiel Termination est nocturne, mais la fonctionnalité elle-même (renvoyant les implémentations Termination partir de main /tests) est déjà stable.

Existe-t-il un ticket ou un délai pour stabiliser le nom du trait ?

@xfix Cela impliquerait qu'il est possible de modifier l'implémentation, n'est-ce pas? Le comportement ne change pas ( Termination renvoyé de main ), mais le trait lui-même changerait de :

impl<E: Debug> Termination for Result<(), E> ...

pour:

impl<E: Display> Termination for Result<(), E> ...

il est possible de changer l'implémentation, non ?

Non sans casser la rétrocompatibilité. Ce code se compile dans le Rust stable d'aujourd'hui :

#[derive(Debug)]
struct X;

fn main() -> Result<(), X> {
    Ok(())
}

Avec la modification proposée pour exiger Display , la compilation cesserait.

Comme cela a déjà été mentionné , il est possible que quelque chose comme la spécialisation puisse aider, mais ma compréhension actuelle de la spécialisation est que cela ne serait utile que pour les types qui implémentent à la fois Display et Debug (c'est-à-dire qu'il y a une implémentation plus spéciale).

trait ResultTerm {
    fn which(&self);
}
impl<T: Debug> ResultTerm for T {
    default fn which(&self) {
        println!("{:?}", self)
    }
}
impl<T: Debug + Display> ResultTerm for T {
    fn which(&self) {
        println!("{}", self)
    }
}

enum MyResult<T, E> {
    Ok(T),
    Err(E),
}

impl<T, E> Termination for MyResult<T, E>
where
    E: ResultTerm,
{
    fn report(self) -> i32 {
        match self {
            MyResult::Err(e) => {
                e.which();
                1
            }
            _ => 0,
        }
    }
}

terrain de jeux

Cela peut suffire à couvrir tous les cas courants, mais cela expose publiquement la spécialisation.

Ah, je vois ce que tu veux dire par rouille stable. Pour une raison quelconque, je ne savais pas que cela était disponible dans stable.

Est-ce quelque chose qui pourrait être commuté au moment de la compilation avec un indicateur Cargo ? De la même manière que d'autres choses sont commutables, comme panic = "abort" .

Peut-être? Je pouvais voir qu'il était possible d'avoir un comportement différent pour ? dans main dans une autre édition de Rust. Probablement pas 2018 cependant, peut-être 2021 ?

Différentes caisses dans le même programme peuvent être dans différentes éditions mais utiliser le même std , donc les traits disponibles dans std et leurs impls doivent être les mêmes d'une édition à l'autre. Ce que nous pourrions faire en théorie (je ne préconise pas cela), c'est avoir deux traits, Termination et Termination2 , et exiger le type de retour de main() pour implémenter l'un ou le autre selon l'édition du crate qui définit main() .

Je ne pense pas que nous devions déprécier quoi que ce soit. Je ferai écho au sentiment de @Aaronepower selon lequel la limitation aux seuls types d'affichage pourrait servir à mécontenter les utilisateurs de scripts rapides (dériver Debug est trivial, implémenter Display ne l'est pas) de la même manière que la situation actuelle mécontente les utilisateurs de production. Même si nous le pouvions, je ne pense pas que ce serait finalement un avantage pour. Je pense que l'approche de spécialisation présentée par @shepmaster est prometteuse, en ce sens qu'elle créerait le comportement suivant :

  1. Un type qui a Debug mais pas Display verra sa sortie Debug imprimée par défaut
  2. Un type qui a à la fois Debug et Display verra sa sortie Display imprimée par défaut
  3. Un type qui a Display mais pas Debug entraîne une erreur du compilateur

Quelqu'un pourrait s'opposer au point 2, dans le cas où vous avez un type avec Display mais que vous voulez qu'il affiche Debug pour une raison quelconque ; Je pense que ce cas serait résolu par la proposition de Niko de divers types d'erreurs de format de sortie prédéfinis (qui valent probablement la peine d'être explorés même si nous suivons la voie de la spécialisation).

En ce qui concerne le point 3, c'est un peu bizarre (peut-être aurions-nous dû faire trait Display: Debug en 1.0 ?), mais si quelqu'un s'est donné la peine d'écrire une implémentation d'affichage, ce n'est pas grand-chose à lui demander une seule dérive (qui, je suppose, ils ont probablement de toute façon ... y a-t-il quelqu'un qui s'oppose par principe à avoir Debug sur son type d'affichage?).

limiter aux seuls types d'affichage pourrait servir à mécontenter les utilisateurs de scripts rapides (dériver Debug est trivial, implémenter Display ne l'est pas)

Un cas où Display est préférable à Debug dans les scripts rapides utilise &'static str ou String comme type d'erreur. Si vous avez un type d'erreur personnalisé sur lequel appliquer #[derive(Debug)] , vous dépensez déjà une énergie non triviale pour la gestion des erreurs.

@SimonSapin Je ne dirais pas que c'est préférable dans un environnement de script rapide, ou plutôt pas assez préférable pour que je sois dans une position où je voulais que la chaîne soit imprimée au format Display sans vouloir faire l'effort pour avoir des erreurs correctement formatées, pour moi, Debug est préféré car le format d'erreur lorsqu'il s'agit de String n'est pas bien formé, donc avoir Err("…") autour me permet de repérer facilement l'erreur de n'importe quelle autre sortie qui peut avoir été émise.

FWIW ce qui est formaté n'est pas la Result<_, E> , seulement la E à l'intérieur. Vous ne verrez donc pas Err( dans la sortie. Cependant, le code actuel ajoute un préfixe Error: , qui resterait vraisemblablement si Display était utilisé. En plus de ne pas imprimer les guillemets, Display n'échapperait pas non plus le contenu de la chaîne, ce qui est souhaitable à l'OMI lorsqu'il s'agit d'afficher un message (d'erreur) aux utilisateurs.

https://github.com/rust-lang/rust/blob/cb6eeddd4dcefa4b71bb4b6bb087d05ad8e82145/src/libstd/process.rs#L1527 -L1533

Je pense que compte tenu de la situation, la meilleure chose que nous puissions faire est de nous spécialiser pour les types qui implémentent Display + Debug pour imprimer l'impl Display . Il est regrettable que les types qui n'implémentent pas Debug ne puissent pas être utilisés comme erreurs (jusqu'à ce que nous prenions en charge les implémentations d'intersection), mais c'est le mieux que nous puissions faire.

La principale question est de savoir si oui ou non cet impl relève de notre politique actuelle pour les impl spécialisés dans les std. Nous avons généralement une règle selon laquelle nous n'utilisons la spécialisation que dans std où nous ne fournissons pas de garantie stable que le comportement ne changera pas plus tard (car la spécialisation elle-même est une fonctionnalité instable). Sommes-nous à l'aise de fournir cette implémentation maintenant en sachant qu'il est possible qu'un jour ces types reviennent à l'impression de leur sortie Debug si nous supprimons finalement la spécialisation en tant que fonctionnalité ?

En fait, je pense que nous ne pouvons pas autoriser cette spécialisation pour le moment, car elle pourrait être utilisée pour introduire des anomalies. Cet impl est en fait exactement le genre de spécialisation qui nous cause tant de problèmes !

use std::cell::Cell;
use std::fmt::*;

struct Foo<'a, 'b> {
     a: &'a Cell<&'a i32>,
     b: &'b i32,
}

impl<'a, 'b> Debug for Foo<'a, 'b> {
    fn fmt(&self, _: &mut Formatter) -> Result {
        Ok(())
    }
}

impl<'a> Display for Foo<'a, 'a> {
    fn fmt(&self, _: &mut Formatter) -> Result {
        self.a.set(self.b);
        Ok(())
    }
}

En raison de la façon dont la spécialisation fonctionne actuellement, je pourrais appeler report sur un Foo<'a, 'b>'b ne survit pas à 'a , et il est actuellement tout à fait possible que le compilateur sélectionnera l'impl qui appelle l'instance Display même si les exigences de durée de vie ne sont pas remplies, permettant à l'utilisateur de prolonger la durée de vie d'une référence de manière non valide.

Je ne sais pas pour vous les gars, mais quand j'écris des "scripts" rapides, je ne fais que unwrap() la merde de tout. C'est 100 fois mieux, car il a des backtraces qui me fournissent des informations contextuelles. Sans cela, au cas où j'aurais let ... = File::open() plus d'une fois dans mon code, je ne peux déjà pas identifier lequel a échoué.

Je ne sais pas pour vous les gars, mais quand j'écris des "scripts" rapides, je ne fais que unwrap() la merde de tout. C'est 100 fois mieux, car il a des backtraces qui me fournissent des informations contextuelles. Sans cela, au cas où j'aurais let ... = File::open() plus d'une fois dans mon code, je ne peux déjà pas identifier lequel a échoué.

C'est en fait pourquoi je suis si attaché à Display , puisque généralement ce que je fais dans ce cas est de faire un map_err qui attache le nom du fichier et d'autres informations contextuelles pertinentes. D'après mon expérience, la sortie de débogage est généralement moins utile pour rechercher ce qui s'est réellement passé.

@Kixunil : Alternativement, je préférerais une stratégie de gestion des erreurs plus élaborée et/ou une journalisation et/ou un débogage plus étendus.

Je suppose que "Implement the RFC" peut être coché, non ? Au moins d'après ça ! :le sourire:

Pardonnez-moi si cela n'a aucune idée, mais ne pourriez-vous pas utiliser la spécialisation pour signaler la chaîne d'erreurs lorsque cela est possible :

use std::error::Error;

impl<E: fmt::Debug> Termination for Result<!, E> {
    default fn report(self) -> i32 {
        let Err(err) = self;
        eprintln!("Error: {:?}", err);
        ExitCode::FAILURE.report()
    }
}

impl<E: fmt::Debug + Error> Termination for Result<!, E> {
    fn report(self) -> i32 {
        let Err(err) = self;
        eprintln!("Error: {:?}", err);

        for cause in Error::chain(&err).skip(1) {
            eprintln!("Caused by: {:?}", cause);
        }
        ExitCode::FAILURE.report()
    }
}

https://github.com/rust-lang/rfcs/blob/f4b8b61a414298ba0f76d9b786d58ccdc34a44bb/text/1937-ques-in-main.md#L260 -L270

impl<T: Termination, E: Display> Termination for Result<T, E> {
    fn report(self) -> i32 {
        match self {
            Ok(val) => val.report(),
            Err(ref err) => {
                print_diagnostics_for_error(err);
                EXIT_FAILURE
            }
        }
    }
}

Y a-t-il une raison pour laquelle cette partie de la RFC n'a pas été implémentée ?

Nous avons apporté quelques modifications aux ensembles précis de traits et d'impls à utiliser, mais je ne me souviens pas des détails à ce stade – et je pense qu'il reste quelques détails à stabiliser. Vous voudrez peut-être consulter les rapports de stabilisation liés au problème principal pour plus de détails.

Quel est l'état de cette fonctionnalité ? Y a-t-il une proposition de stabilisation?

@GrayJack Ce problème devrait être fermé, car il est arrivé il y a un certain temps.

C'est vrai, et je suis un peu confus, je suis arrivé ici à partir du lien dans le trait Termination et je demandais à ce sujet, y a-t-il un problème approprié pour termination_trait_lib ?

Ce sujet devrait être clos

Ce problème est toujours ouvert pour suivre la stabilisation de Termination .

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