Rust: Problema de rastreamento para RFC 1937: `?` em `main`

Criado em 18 jul. 2017  ·  183Comentários  ·  Fonte: rust-lang/rust

Este é um problema de rastreamento para o RFC " ? em main " (rust-lang/rfcs#1937).

Degraus:

Estabilizações:

  • [x] Estabilize main com tipos de retorno não () (https://github.com/rust-lang/rust/issues/48453) Mesclado em https://github.com/rust-lang/ ferrugem/puxar/49162
  • [x] Estabilize testes de unidade com tipos de retorno não () (https://github.com/rust-lang/rust/issues/48854)

Assuntos relacionados:

  • [x] A mensagem de erro para testes de unidade não é ótima (https://github.com/rust-lang/rust/issues/50291)

Perguntas não resolvidas:

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

Comentários muito úteis

Desculpas por entrar em contato em um ponto em que provavelmente é tarde demais para fazer algo sobre isso, mas queria deixar meu feedback aqui, caso houvesse. Eu li a maior parte deste tópico, então falo com esse contexto em mente. No entanto, este tópico é longo, então se parece que eu esqueci de algo, então eu provavelmente o fiz e eu gostaria de tê-lo apontado para mim. :-)

TL;DR - Acho que mostrar a mensagem Debug de um erro foi um erro, e que uma escolha melhor seria usar a mensagem Display de um erro.

No centro da minha crença é que, como alguém que _rotineiramente constrói programas CLI em Rust_, eu não consigo me lembrar de me importar muito com o que é a mensagem Debug de um Error . Ou seja, o Debug de um erro é, por design, para desenvolvedores, não para usuários finais. Quando você cria um programa CLI, sua interface é fundamentalmente destinada a ser lida pelos usuários finais, então uma mensagem Debug tem muito pouca utilidade aqui. Ou seja, se qualquer programa CLI que enviei para usuários finais mostrasse a representação de depuração de um valor Rust em operação normal, então eu consideraria que um bug foi corrigido. Eu geralmente acho que isso deve ser verdade para todos os programas CLI escritos em Rust, embora eu entenda que pode ser um ponto em que pessoas razoáveis ​​podem discordar. Com isso dito, uma implicação um tanto surpreendente da minha opinião é que efetivamente estabilizamos um recurso em que seu modo de operação padrão inicia com um bug (novamente, IMO) que deve ser corrigido.

Ao mostrar a representação Debug de um erro por padrão, também acho que estamos incentivando a má prática. Em particular, é muito comum durante a escrita de um programa Rust CLI observar que mesmo o Display impl de um erro não é bom o suficiente para ser consumido pelos usuários finais, e que um trabalho real deve ser feito para consertá-lo. Um exemplo concreto disso é io::Error . Mostrar um io::Error sem um caminho de arquivo correspondente (assumindo que veio da leitura/escrita/abertura/criação de um arquivo) é basicamente um bug, porque é difícil para um usuário final fazer qualquer coisa com ele. Ao optar por mostrar a representação Debug de um erro por padrão, tornamos mais difícil que esses tipos de bugs sejam descobertos por pessoas que criam programas CLI. (Além disso, o Debug de um io::Error é muito menos útil do que o Display , mas isso por si só não é um grande ponto de dor na minha experiência. )

Finalmente, para completar meu argumento, também tenho dificuldade em visualizar as circunstâncias sob as quais eu usaria ?-in-main mesmo em exemplos. Ou seja, tenho tentado escrever exemplos que correspondam aos programas do mundo real o mais próximo possível, e isso geralmente envolve escrever coisas assim:

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);
    }
}

À primeira vista, seria _lovely_ substituir isso por ?-in-main , mas não posso, porque não mostrará o Display de um erro. Ou seja, ao escrever um programa CLI real, usarei de fato a abordagem acima, portanto, se quiser que meus exemplos reflitam a realidade, acho que devo mostrar o que faço em programas reais e não usar atalhos (de forma razoável ). Na verdade, acho que esse tipo de coisa é realmente importante, e um efeito colateral disso historicamente foi que mostrou às pessoas como escrever código Rust idiomático sem espalhar unwrap em todos os lugares. Mas se eu voltar a usar ?-in-main em meus exemplos, acabei de renegar meu objetivo: agora estou configurando pessoas que podem não saber nada para escrever programas que, por padrão, emitem muito mensagens de erro inúteis.

O padrão de "emitir uma mensagem de erro e sair com um código de erro apropriado" é realmente usado em programas polidos. Por exemplo, se ?-in-main usasse Display , então eu poderia mesclar as funções main e run no ripgrep hoje:

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

Claro, eu poderia usar ?-in-main no futuro fornecendo meu próprio impl para o traço Termination assim que estabilizar, mas por que eu me incomodaria em fazer isso se eu pudesse escrever o main função que eu tenho? E mesmo assim, isso não ajuda no meu enigma ao escrever exemplos. Eu precisaria incluir esse impl nos exemplos para torná-los realidade, e nesse ponto, eu poderia ficar com os exemplos que tenho hoje (usando um main e try_main ).

Pelo que parece, parece que consertar isso seria uma mudança radical. Ou seja, este código compila no Rust estável hoje:

#[derive(Debug)]
struct OnlyDebug;

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

Acho que mudar para Display quebraria esse código. Mas não tenho certeza! Se esta é realmente uma questão de navio já partiu, então eu entendo e não há muito uso em aprofundar o ponto, mas eu me sinto forte o suficiente sobre isso pelo menos para dizer algo e ver se não consigo convencer os outros e ver se houver algo que possa ser feito para corrigi-lo. (Também é bem possível que eu esteja exagerando aqui, mas até agora algumas pessoas me perguntaram "por que você não está usando ?-in-main ?" em meus exemplos de CSV, e minha resposta foi basicamente, "Não vejo como seria viável fazer isso." Talvez esse não fosse um problema que deveria ser resolvido por ?-in-main , mas algumas pessoas certamente tiveram essa impressão. Com sua implementação atual , eu poderia vê-lo sendo útil em testes de documentos e testes de unidade, mas tenho dificuldade em pensar em outras situações em que eu o usaria.)

Todos 183 comentários

Como os status de saída serão tratados?

Este comentário de @Screwtapello parece ter sido feito muito perto do fim do FCP para que quaisquer alterações fossem feitas no RFC em resposta a ele.

Resumindo: a RFC propõe a devolução de 2 por falha por motivos que, embora bem fundamentados, são obscuros e produzem um resultado um pouco incomum; o menos surpreendente é retornar 1 quando o programa não tem indicação de que deseja mais detalhes do que apenas sucesso ou fracasso. Isso é suficiente para que possa ser discutido sem parecer que estamos pervertendo o processo RFC, ou agora estamos presos a esse detalhe específico de implementação?

Não é um detalhe de implementação, é?

Alguns scripts usam códigos de saída como forma de obter informações de um subprocesso.

Isso é especificamente sobre o caso em que um subprocesso (implementado em Rust) não tem informações para fornecer, além de um binário "está tudo bem"/"algo deu errado".

Alguns scripts usam códigos de saída como forma de obter informações de um subprocesso.

Esse comportamento é sempre extremamente dependente do programa que está sendo chamado _except_ em que diferente de zero significa falha. Dado que std::process::exit com um wrapper de função principal e uma tabela de pesquisa continuará sendo a melhor opção para aqueles que desejam um status de saída mais articulado, não importa o que seja feito, isso parece um detalhe insignificante.

Eu não acho que SemVer tenha uma exceção de "detalhe principalmente insignificante".

Acho que o código de saída deve ser adicionado à lista de perguntas não resolvidas. @zackw também abriu um thread interno relacionado.

Muitas pessoas concordam que o código de saída deve ser 1 em caso de falha (em vez de 2 ):
https://www.reddit.com/r/rust/comments/6nxg6t/the_rfc_using_in_main_just_got_merged/

@arielb1 você vai implementar este rfc?

@bkchr

Não, apenas para orientá-lo. Eu atribuí para não esquecer de escrever as notas de orientação.

Ahh legal, eu estaria interessado em fazê-lo :)
Mas não sei por onde começar :D

@bkchr

É por isso que estou aqui :-). Devo escrever as instruções de orientação em breve.

Ok, então estou esperando por suas instruções.

Instruções de mentoria

Este é um problema [WG-compiler-middle]. Se você quiser procurar ajuda, pode se juntar a #rustc no irc.mozilla.org (sou arielby) ou https://gitter.im/rust-impl-period/WG-compiler-middle (sou @arielb1 lá).

Há um leia-me do compilador WIP em #44505 - ele descreve algumas coisas no compilador.

Plano de trabalho para este RFC:

  • [ ] - adiciona o Termination lang-item ao libcore
  • [ ] - permite usar Termination em main
  • [ ] - permite usar Termination em doctests
  • [ ] - permite usar Termination em #[test]

adicione o Termination lang-item à libcore

Primeiro, você precisa adicionar a característica Termination a libcore/ops/termination.rs , junto com alguma documentação. Você também precisará marcá-lo como instável com um atributo #[unstable(feature = "termination_trait", issue = "0")] - isso impedirá que as pessoas o usem antes que ele seja estabilizado.

Então, você precisa marcá-lo como um lang-item em src/librustc/middle/lang_items.rs . Isso significa que o compilador pode descobrir isso ao verificar main (por exemplo, veja 0c3ac648f85cca1e8dd89dfff727a422bc1897a6).
Que significa:

  1. adicionando-o à lista de itens lang (em librustc/middle/lang_items.rs )
  2. adicionando um #[cfg_attr(not(stage0), lang = "termination")] ao traço Termination . A razão pela qual você não pode simplesmente adicionar um atributo #[lang = "termination"] é porque o compilador "stage0" (durante a inicialização) não saberá que termination é algo que existe, então não será capaz de compilar libstd. Removeremos manualmente o cfg_attr quando atualizarmos o compilador stage0.
    Consulte os documentos de bootstrap em XXX para obter detalhes.

permitir o uso de Termination no main

Esta é a parte interessante com a qual sei lidar. Isso significa fazer um main que retorna () sem verificação de tipo (atualmente você recebe um erro main function has wrong type ) e funcionar.

Para fazer a verificação de tipo, primeiro você precisa remover o erro existente em:
https://github.com/rust-lang/rust/blob/9a00f3cc306f2f79bfbd54f1986d8ca7a74f6661/src/librustc_typeck/lib.rs#L171 -L218

Então, você precisa adicionar uma verificação de que o tipo de retorno implementa a característica Termination (você adiciona uma obrigação de característica usando register_predicate_obligation - procure por usos disso). Isso pode ser feito aqui:
https://github.com/rust-lang/rust/blob/9a00f3cc306f2f79bfbd54f1986d8ca7a74f6661/src/librustc_typeck/check/mod.rs#L1100 -L1108

A outra parte é fazer funcionar. Isso deve ser bastante fácil. Como diz o RFC, você deseja tornar lang_start genérico sobre o tipo de retorno.

lang_start está atualmente definido aqui:
https://github.com/rust-lang/rust/blob/9a00f3cc306f2f79bfbd54f1986d8ca7a74f6661/src/libstd/rt.rs#L32

Portanto, você precisará alterá-lo para ser genérico e corresponder ao 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
    });
}

E então você precisará chamá-lo de create_entry_fn . Atualmente, ele instancia um lang_start monomórfico usando Instance::mono , e você precisará alterá-lo para usar monomorphize::resolve com as substs corretas.

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

permitir o uso Termination em doctests

Eu realmente não entendo como os doctests funcionam. Talvez pergunte a @alexcrichton (isso é o que eu faria)?

permitir o uso Termination em #[test]

Eu realmente não entendo como o libtest funciona. Talvez pergunte a @alexcrichton (isso é o que eu faria)? Os testes de unidade são basicamente gerados por uma macro, portanto, você precisa alterar essa macro, ou seu chamador, para lidar com tipos de retorno que não são () .

@bkchr

Você pode pelo menos se juntar ao IRC/gitter?

@bkchr apenas verificando - eu vi você e @arielb1 conversando no gitter algum tempo atrás, algum progresso? Chupar em algum lugar?

Não desculpe, nenhum progresso até agora. Atualmente, tenho muitas coisas para fazer, mas espero encontrar algum tempo esta semana para começar isso.

@bkchr Se precisar de ajuda, me avise!

Atualmente estou um pouco travado, quero criar a Obrigação. Para criar a Obrigação preciso de um TraifRef, para um TraitRef preciso de um DefId. Alguém pode me apontar para algum código sobre como criar um DefId a partir do traço de terminação?

Sim, esse não é o problema, eu já fiz isso. Eu preciso verificar o traço de terminação na função check_fn. Eu quero usar register_predicate_obligation e para isso preciso do defid do traço de terminação.

Ah, então tudo que você precisa é tcx.require_lang_item(TerminationTraitLangItem) .

@bkchr como vai? Apenas verificando novamente. =) Não se preocupe se você estiver ocupado, só quero ter certeza de que está recebendo toda a ajuda de que precisa.

Desculpe, estou ocupado no momento :/ Até agora, consegui toda a ajuda que precisava :)

Este é o código para verificar o TerminationTrait: https://github.com/bkchr/rust/blob/f185e355d8970c3350269ddbc6dfe3b8f678dc44/src/librustc_typeck/check/mod.rs#L1108

Acho que não estou verificando o tipo de retorno da função? Estou tendo o erro a seguir:

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

O que preciso mudar, para verificar o tipo de retorno da função?

@bkchr Eu recomendo entrar no gitter do grupo de trabalho do compilador-middle em https://gitter.im/rust-impl-period/WG-compiler-middle para feedback, bem como tentar o canal de IRC #rust-internals em https ://chat.mibbit.com/?server=irc.mozilla.org%3A%2B6697&channel=%23rust-internals . :)

@bstrie sim obrigado, já faço parte do chat do gitter e consegui resolver meu problema. :)

@bkchr seu problema está nesta linha . A referência de trait que você quer construir é algo como R: Termination onde R é o tipo de retorno da função. Isso é especificado construindo um "substs" apropriado, que é o conjunto de valores para substituir os parâmetros de tipo da característica (neste caso, Self ).

No entanto, você está invocando o método Substs::identity_for_item na característica. Isso lhe devolverá as substituições que seriam usadas dentro da própria definição de traço . ou seja, neste caso você está mapeando o parâmetro Self declarado na característica Termination para Self . Isso seria apropriado se você estivesse verificando a definição de alguma função dentro da característica Terminator , mas não tanto aqui.

O que você quer é obter o tipo de retorno da função de entrada. Esta é apenas uma das variáveis ret_ty ou actual_ret_ty . Qualquer um é bom, mas acho que ret_ty é melhor -- que corresponde ao tipo de retorno que o usuário declarou (enquanto actual_ret_ty é o tipo que o código real retornou).

Você pode fazer os substs que deseja apenas chamando o método mk_substs() do tcx. Nesse caso, há apenas um parâmetro, o tipo, então algo como let substs = fcx.tcx.mk_substs(&[ret_ty]); funcionaria, eu acho.

Eu acredito que a coisa a usar é tcx.mk_substs_trait(ret_ty, &[]) .

@bkchr apenas fazendo check-in - teve a chance de usar esse conselho? (Além disso, para respostas mais rápidas, pode ser sensato perguntar no gitter .)

Sim, eu poderia resolver o problema com o gitter :)

@bkchr como vai? Apenas checando.

Tudo bem, provavelmente terei algum tempo esta semana para analisar o código.

Há espaço para mais uma pessoa para ajudar com isso? Eu gostaria de começar a contribuir para a comunidade Rust antes do final do ano e adoraria ajudar com esse recurso. Espero que não seja muito confuso ter duas pessoas colaborando nisso.

@U007D

Este é um pequeno recurso e o @bkchr está quase pronto.

Ah, ok - é bom saber, obrigado. Vou ficar de olho em outra coisa que eu possa ajudar.

@U007D Você já viu https://www.rustaceans.org/findwork ?

@lnicola Sim, eu tenho! Estou tentando encontrar algo na interseção de algo em que me sinto confiante em poder trabalhar (ou seja, ser um positivo líquido) e pelo qual sou apaixonado. Para ser honesto, embora eu esteja aprendendo Rust há cerca de um ano, ainda é um pouco intimidante me apresentar como voluntário para alguma coisa. FWIW, isso não é culpa da comunidade Rust - a comunidade Rust se esforçou para tornar isso uma cultura aberta, acolhedora e inclusiva - a melhor que tive o prazer de experimentar. (Suspeito que tenha mais a ver com antigas cicatrizes de batalha de anos e anos de experiência na indústria de tecnologia, onde as equipes tendem a ser competitivas em vez de colaborativas.)

De qualquer forma, é meu objetivo escolher algo este ano e pelo menos começar a dar uma contribuição positiva. É hora de eu me envolver! :)

Obrigado pela sugestão, @lnicola. Isso é um bom recurso.

@bkchr alguma atualização?

Estou nisso (https://github.com/rust-lang/rust/pull/46479). Agora tenho feriados e tempo para trabalhar nos comentários no pull request. Desculpem todos os atrasos :/

Oh, desculpe, não notei que você tinha um pull request. Cruzou-o.

Olá, hum. Então eu pensei em começar minha carreira de contribuidor de Rust potencial por bikeshedding, como é tradição. Especificamente, sobre este:

  • [ ] O nome da característica que está sendo introduzida

Que tal Exit ? É curto e direto ao ponto, se encaixando no vocabulário Rust existente. Sair-como-um-substantivo é uma contrapartida natural para sair-como-verbo que, para a maioria, é a palavra familiar para terminar um processo "de dentro" de maneira controlada.

Para um programador C++ especificamente, "termination" traz à mente std::terminate que é padronizado para terminação anormal (chamando abort ) e é basicamente o equivalente em C++ a um panic (mas diferentemente de um panic, nunca desenrola o pilha).

Espere, ignore esse comentário, parece que a RFC deixou isso explicitamente aberto à discussão.

Eu gosto de Exit como um nome de traço.

Estou imaginando que o recurso será estabilizado muito antes do traço, como aconteceu com Carrier .

FWIW esse é outro caso em que estou muito feliz que o nome provisório foi alterado antes da estabilização :D

Como autor do RFC, não tenho objeções a alterar o nome do traço para Exit , ou qualquer outra coisa. Não sou particularmente bom em nomear as coisas e fico feliz que outra pessoa tenha uma ideia melhor.

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

O traço deveria ser

  1. colocado em libstd em vez de libcore, e
  2. acabou de chamar std::Termination , não std::ops::Termination ?

O traço não pôde ser colocado em libcore , porque a implementação de Result requer imprimir em stderr e isso não pode ser feito em libcore .

@bkchr O impl estar em libstd não significa que o traço precisa estar em libstd também.

@kennytm Eu sei, mas Result também é definido em libcore, então Termination não pode ser implementado para Result em libstd.

@zackw +1 voto para Exit como o nome da característica.

@U007D : Você poderia usar o botão de reações (por exemplo, 👍) em vez de postar essa mensagem? Isso permitiria que você evitasse os assinantes de problemas irritantes, pingando-os desnecessariamente.

Posso fazer check-in libtest / libsyntax , se um language_feature estiver ativado (em uma caixa)? @arielb1 @nikomatsakis @alexcrichton

@bkchr em libsyntax você pode ter que passá-lo, mas em teoria é possível, mas no próprio libtest em tempo de execução eu não acredito que você possa verificar.

@bkchr como vai aqui?

Ainda estou trabalhando nisso, mas no momento não tenho mais perguntas :)

Eu acho que este impl é muito rigoroso:

#[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());
    }
}

Existem vários erros comumente usados ​​que não implementam Error , o mais importante Box<::std::error::Error> e failure::Error . Também acho um erro usar o método description ao invés do display impl deste erro.

Eu proporia substituir este impl por este impl mais amplo:

#[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
            }
        }
    }
}

Isso perde a cadeia de causas que é uma chatice.

Usar o display impl em vez da descrição é definitivamente a melhor coisa a fazer.

A cadeia de causas é um problema interessante. Em particular, essa implementação imprime apenas os dois primeiros membros da cadeia de causas.

falhas tiveram que lidar com como lidar com a cadeia de causas e estabeleceram esse comportamento por padrão (por exemplo, se você acabou de criar erros usando .context :

  • {} imprime apenas este erro
  • {:?} imprime este erro, bem como sua causa (recursivamente)

Poderíamos decidir usar :? aqui e vinculá-lo Debug em vez de Display. Inseguro.

Pois é, já sei que preciso melhorar o impl para dar suporte. Estou aberto sobre o que podemos fazer. Vincular a Debug pode ser uma boa ideia.

Hmm, isso é complicado. Eu acho que depende se pensamos que um programa "polido" fará uso dessa característica impl. Eu costumo pensar que não há problema em dizer que "não, eles não vão" - basicamente, um programa polido (a) pegará a saída e lidará com ela de alguma outra maneira ou (b) usará algum newtype ou algo que implemente Debug o caminho certo. Isso significaria que podemos otimizar o impl para despejar informações úteis, mas necessariamente na forma mais bonita (que parece o papel de Debug ).

Pode ser a escolha certa tornar isso claramente direcionado à prototipagem usando Debug , já que não acho que poderíamos lidar automaticamente com erros de uma maneira correta para a maioria dos casos de uso de produção.

@withoutboats eu concordo.

@nikomatsakis Presumo que você quis dizer " não necessariamente da forma mais bonita"? Se sim, sim, concordo.

Atualização: depois de trabalhar nisso por alguns dias, eu liguei isso. Veja abaixo.

:+1: em Debug , aqui; Eu gosto do "tipo de análogo a uma exceção não capturada" de @nikomatsakis de https://github.com/rust-lang/rfcs/pull/1937#issuecomment -284509933. Um comentário de Diggsey também sugerindo Debug : https://github.com/rust-lang/rfcs/pull/1937#issuecomment -289248751

Para sua informação, liguei o problema padrão "mais completo" versus "mais fácil de usar" (ou seja Debug vs Display vinculado ao traço).

O TL;DR é que agora acredito que devemos definir o limite para Display (conforme a postagem original de @withoutboats ) para fornecer uma saída de resumo mais limpa no caso de 'não fazer nada'.

Segue meu raciocínio:

Na questão RFC do traço termination , @zackw ressalta que Rust tem o sistema duplo panic / Result porque panic s são para bugs e Result s são para erros. A partir disso, acho que um argumento convincente pode ser feito para avaliar a apresentação de erro padrão independentemente da apresentação de pânico padrão.

Claro que não existe um default que satisfaça a todos, então aplicando o princípio da menor surpresa, eu me pergunto qual default é mais apropriado?

  • Um erro geralmente não é tratado por design , pois se destina a ser comunicado ao usuário que algo (possivelmente corrigível) deu errado (arquivo não encontrado etc.). Como tal, o caso de uso existe e pode ser razoavelmente considerado comum que o usuário seja o público-alvo.

  • Como @nikomatsakis apontou, independentemente do padrão escolhido, qualquer desenvolvedor que deseje alterar o comportamento pode usar o padrão newtype ou desenvolver uma implementação personalizada em main().

E, finalmente, no lado mais subjetivo, ao trabalhar com esse recurso nos últimos dois dias, descobri que a saída Debug me deixou com a sensação de que meu "aplicativo Rust" parecia menos polido :

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

vs

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

O traço Dispay parece ser um padrão muito mais civilizado para um erro (em oposição a um bug), não é?

@U007D espere, qual dessas duas saídas você prefere?

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

ou

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

Ele prefere a opção (b).

@nikomatsakis Originalmente, eu estava bem com a) Debug como um conceito na minha cabeça, mas depois de trabalhar com isso por alguns dias realmente vendo a saída, agora prefiro b) Display como um padrão. Acho que minha preferência por b) se tornaria ainda mais pronunciada se eu estivesse modelando um erro encadeado.

No entanto, não acho que "polido" ou "civilizado" seja o objetivo disso, pois entendi que o tópico já havia aceitado isso como sendo principalmente para exemplos, com as pessoas totalmente esperadas para adicionar tratamento personalizado à medida que o programa amadurece.

Nesses casos, para mim, "menos surpresa" é uma saída orientada ao desenvolvedor, como unwrap .

Valeria a pena discutir {:#?} aqui, se houver preocupação com um erro longo?

O relatório de erros para usuários finais será diferente para cada ferramenta e cada caso de uso, mas o relatório de erros para desenvolvedores deve se assemelhar ao que Rust faz em outras situações como .unwrap() . Como só pode haver um padrão, e o software polido precisará substituir a saída de qualquer maneira, eu voto para que o padrão seja orientado ao desenvolvedor com Debug .

Acho que o cerne desta discussão é realmente "quem é o público-alvo da mensagem padrão?"

Digamos por um momento que todos concordamos que o público-alvo padrão eram os desenvolvedores. Eu acho que o limite padrão Debug seria uma escolha direta.

Agora digamos por um momento que concordamos que o público-alvo padrão era o usuário, então é aqui que, respeitosamente, discordo de alguns outros e sinto que qualidades subjetivas como saída "polida" e "civilizada" têm uma parte importante a ser Reproduzir. Para alguns, a apresentação "polida" para o usuário final pode ser a melhor razão de todas para evitar Display . (Não compartilho dessa visão, mas a entendo e a respeito.)

Então, sim, certamente posso ver argumentos razoáveis ​​para qualquer grupo como o alvo padrão. Eu acho que se um forte consenso se desenvolver em torno de qual público deve ser o alvo padrão, então a escolha para o traço vinculado será clara (er)... :)

(Eu não sou totalmente versado em todo este tópico, mas) não é concebível que possa haver pequenos utilitários para os quais a saída de erro padrão com Termination seja perfeitamente adequada, desde que esteja em algum formato apresentável pelo usuário como Display ? Nesse caso, a única razão pela qual os autores teriam que usar o "manuseio personalizado" é se nós os fizermos.

Alguém pode fornecer exemplos de como a saída se parece em cada caso (suponho que também dependa do tipo E específico usado?), e quais etapas os autores realmente precisam seguir se quiserem "manuseio personalizado" em vez de? Estou apenas indo em hipóteses acima.

(A saída literalmente se parece com o que @U007D colou acima? Por que ele imprimiria "caixa retornada\Caixa<Erro>?)

Com que frequência o Display da mensagem de erro é amigável o suficiente? Por exemplo, o seguinte programa:

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

emite a seguinte mensagem:

No such file or directory (os error 2)

O que, eu diria, não é um ótimo UX, especialmente com o nome do arquivo não mencionado. Pelo menos não, a menos que o programa literalmente leve um único nome de arquivo como entrada. Por outro lado, também não é uma ótima experiência de desenvolvedor , perdendo o arquivo de origem/número de linha/rastreamento de pilha. A saída Debug é, obviamente, uma experiência de usuário ainda pior e também não adiciona informações úteis para o deceloper.

Então, acho que o que estou tentando dizer é que sem melhorar o conteúdo da informação dos erros da biblioteca, nem Debug nem Display são ótimos.

A saída literalmente se parece com o que @U007D colou acima? Por que imprimiria "caixa devolvidafrom main()" em vez de... o conteúdo real dessa caixa?

@glaebhoerl Você está correto - neste caso, o "conteúdo real desse Box<Error> " era um campo message personalizado que eu criei para testar o termination_trait , exibido literalmente . Eu poderia ter escrito "foo bar baz" ou qualquer outra coisa lá (mas isso pode não ter sido tão útil para um usuário executando os testes do compilador).

Usando o exemplo de @jdahlstrom , aqui está a saída real para uma biblioteca padrão Box ed "arquivo não encontrado" Error (observe, como você aponta corretamente, nenhuma menção ao boxe em qualquer lugar):
Debug :

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

e Display :

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

@jdahlstrom , acho que você faz um bom ponto. Concordo que, embora ambas as mensagens possam não atender ao público-alvo e quero destacar que fornecer a mensagem errada é ainda pior (como acho que você aludiu):

Fornecer Display a um desenvolvedor tem todas as desvantagens de Debug além de perder a especificidade de qual tipo de erro está sendo exibido.

Fornecer Debug a um usuário tem todas as desvantagens de Display além de adicionar ainda mais informações técnicas que o usuário não precisa e pode não ser capaz de entender.

Então, sim, concordo que as mensagens geralmente não são direcionadas o suficiente para nenhum dos públicos. Acho que isso destaca outra razão importante para sermos claros sobre quem estamos direcionando para fornecer a melhor experiência possível (não obstante as falhas) para esse grupo.

Preciso de ajuda para implementar o suporte para ? em #[test] . Minha implementação atual pode ser encontrada aqui: https://github.com/rust-lang/rust/compare/master...bkchr :termination_trait_in_tests

A compilação de um teste com minhas alterações resulta no seguinte erro:

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

@eddyb disse que eu não deveria mais usar quote_item!/expr! , porque eles são legados.
O que devo fazer agora, mudar para a nova macro quote! ou retrabalhar tudo para a construção manual ast?

Agradeço qualquer ajuda :)

Acho que gerar uma invocação de macro para alguma macro definida em libtest poderia funcionar muito bem.

@eddyb Não tenho certeza se entendi sua sugestão aqui:

Acho que gerar uma invocação de macro para alguma macro definida na libtest poderia funcionar muito bem.

Oh, eu acho que talvez eu faça. Você está dizendo -- definir uma macro no libtest e então gerar um código que a invoque? Ideia interessante. Mas esse nome de macro não "vazaria"? (ou seja, torna-se parte da interface pública do libtest?)


@bkchr

A compilação de um teste com minhas alterações resulta no seguinte erro:

Você tem alguma idéia de por que esse erro está sendo gerado? Apenas lendo o diff, eu não, mas posso tentar construir localmente e descobrir.

Não devo mais usar quote_item!/expr! , porque eles são legados.

Eu não tenho uma opinião forte aqui. Concordo com @eddyb , eles são legados, mas não tenho certeza se são o tipo de legado em que adicionar mais alguns usos os tornará mais difíceis de remover - ou seja, assim que obtivermos uma substituição recente, seria fácil @eddyb para passar de um para o outro?

A construção manual de AST é certamente uma dor, embora eu ache que temos alguns ajudantes para isso.

Na maioria das vezes, só precisamos fazer uma pequena edição, certo? ou seja, mudar de invocar a função para testar o resultado de report() ?

PS, provavelmente queremos gerar algo como Termination::report(...) em vez de usar uma notação .report() , para evitar depender do traço Termination estar no escopo?

Não, não faço ideia de onde vem esse erro :(

A RFC propôs gerar uma função wrapper que chama a função de teste original. Essa também é a minha maneira atual de fazê-lo.
Eu acho que também poderíamos descartar a função wrapper, mas então exigiríamos o boxing do ponteiro de função, pois cada função de teste pode retornar um tipo diferente.

Hmm, como o teste já importa outras coisas, não é tão complicado importar também o traço Termination.

@alexcrichton você talvez tenha uma ideia de onde vem esse erro?

@nikomatsakis libtest é instável e também não podemos marcar macros como instáveis, mesmo que não fossem?

@eddyb oh, bom ponto.

A RFC propôs gerar uma função wrapper que chama a função de teste original. Essa também é a minha maneira atual de fazê-lo.

Uma função wrapper parece bem para mim.

@eddyb com a macro você quer dizer algo como create_test que está definido em libtest ? Mas o que não entendo é "marcar macro como instável". O que você pretende com isso? Você poderia me dar um exemplo?

@bkchr Colocando um atributo #[unstable(...)] na definição da macro, por exemplo: https://github.com/rust-lang/rust/blob/3a39b2aa5a68dd07aacab2106db3927f666a485a/src/libstd/thread/local.rs#L159 -L165

Então, essa primeira caixa de seleção...

Implemente o RFC

...ser verificado agora que o PR vinculado foi mesclado?

@ErichDonGubler Pronto :)

Hmm, atualmente é apenas o meio rfc que é implementado com o pr mesclado ^^

Separado em 3 caixas :)

Eu tenho tentado usar esse recurso em main e achei muito frustrante. Os impls existentes da característica de terminação simplesmente não me permitem "acumular" convenientemente vários tipos de erros - por exemplo, não posso usar failure::Fail , porque não implementa Error ; Não consigo usar Box<Error> , mesmo motivo. Acho que devemos priorizar a mudança para Debug . =)

Olá, @nikomatsakis ,

Senti exatamente a mesma frustração que você quando tentei usar termination_trait .

Isso, combinado com suas postagens sobre hacking no compilador, me inspirou a tentar resolver esse problema no início deste mês. Eu postei o impl para Display (e para Debug no commit anterior) junto com os testes aqui: https://github.com/rust-lang/rust/pull/47544. (É muito menor, mas ainda assim, meu primeiro compilador Rust PR! :tada:) :)

Meu entendimento é que a equipe lang fará uma chamada sobre qual característica seguir, mas de qualquer forma, a implementação está pronta.

Uma pergunta em que ainda estou interessado: suponha que você não queira confiar na saída da mensagem de erro padrão (seja Debug ou Display ) e queira a sua própria, como você faz aquele? (Desculpe se isso já foi escrito em algum lugar e eu perdi.) Você não teria que parar de usar ? -in- main inteiramente, não é? É algo como escrever seu próprio resultado e/ou tipo de erro e/ou impl ? (Parece lamentável para mim se ? -in- main fosse apenas um brinquedo, e assim que você quisesse "ficar sério" você teria que reverter para formas menos ergonômicas.)

@glaebhoerl É muito simples:

  1. Crie um novo tipo.
  2. Implemente From seu tipo de erro antigo.
  3. Implemente Debug (ou Display ).
  4. Substitua o tipo na assinatura de main .

Obrigado!

Parece um pouco estranho para mim escrever implementações personalizadas de Debug que não são orientadas para depuração, mas acho que não é o fim do mundo.

@glaebhoerl É por isso que o Result impl deve usar Display em vez de Debug IMO.

O traço Termination não pode ter um método extra chamado message , ou error_message ou algo assim, que tem uma implementação padrão que usa o Debug / Display para mostrar a mensagem? Então você só precisa implementar um único método em vez de criar um novo tipo.

@glaebhoerl @Thomasdezeeuw Algo como um método error_message estava no rascunho original da RFC, mas foi descartado por falta de consenso. Minha sensação na época era que seria melhor obter o recurso básico (não necessariamente estabilizado) e depois iterar.

@zackw Concordo, eu pessoalmente ficaria bem sem mensagens, apenas um número, digamos 0 e 1 para códigos de erro. Mas se quisermos obter as mensagens na primeira iteração, acho que seria mais a favor de um Termination::message do que qualquer coisa em Debug ou Display .

Esse método message retornaria um String ? Isso não seria incompatível com Termination estar na libcore?

@SimonSapin Termination está atualmente definido em libstd .

Hmm, mas eu não acho que adicionar um método message ao traço seria a melhor implementação. O que o método retornaria para tipos como i32 ? Como decidiria quando imprimir esta mensagem? Atualmente a implementação de Termination para Result , imprime o erro na função report . Isso funciona, porque Result sabe que é um Err . Poderíamos integrar em algum lugar um cheque report() != 0 e depois imprimir, mas isso não parece certo.
O próximo problema seria que queremos fornecer uma implementação padrão para Result , mas o que o tipo Error precisa implementar para ser impresso provavelmente? Isso nos traria de volta à questão atual Debug ou Display .

Outra dor de cabeça que surge se você quiser usar ? no main em um programa "sério" é que, em algumas circunstâncias, os programas de linha de comando querem sair com um status diferente de zero, mas sem imprimir _qualquer coisa_ (considere grep -q ). Então agora você precisa de um Termination impl para algo que _não é_ um Error , que não imprime nada, que permite controlar o status de saída ... e você precisa decidir se quer ou não está retornando essa coisa _depois_ analisando os argumentos da linha de comando.

Isto é o que eu penso:

Retornar Result<T, E> deve usar o debug impl para E. Essa é a característica mais conveniente -- é amplamente implementada e satisfaz o caso de uso "saída rápida e suja" bem como o caso de uso de teste de unidade. Prefiro não usar Display porque é menos implementado e porque dá a impressão de que essa é uma saída polida, o que acho altamente improvável.

Mas também deve haver uma maneira de obter uma saída com aparência profissional. Felizmente, já existem duas maneiras. Primeiro, como @withoutboats disse, as pessoas podem fazer uma ponte "display-from-debug" se quiserem usar E: Display ou fornecer saída de estilo profissional. Mas se isso parecer muito hacky, você também pode definir seu próprio tipo para substituir o resultado. por exemplo, você pode fazer:

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

e então implemente Try para ProfessionalLookingResult . Então você pode implementar Terminate também, seja o que for:

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

Eu concordo com @nikomatsakis que isso deveria usar Debug .

Eu também acho que para uma saída polida, escrever algum código em main é provavelmente melhor do que criar um novo tipo para implementar Try e Terminate . O ponto de Terminate me parece ser para coisas que as bibliotecas podem facilmente fazer um bom padrão, o que nunca é o caso de uma situação em que a forma como o programa termina é significativo para os usuários finais (por exemplo, CLIs profissionais) .

É claro que outras pessoas podem ter uma opinião diferente, e existem várias maneiras de usar as características envolvidas para injetar código em vez de escrevê-lo diretamente em main . O que é ótimo é que temos várias opções e não temos que encontrar uma única maneira abençoada de lidar com erros sempre.

Deixe-me apenas anotar alguns pensamentos, embora haja alguns problemas com eles.

Eu adoraria ver o seguinte

fn main() -> i32 {
    1
}

Que poderia ser escrito de forma mais geral como:

fn main() -> impl Display {
    1
}

Ambas as funções principais devem retornar um código de saída 0 e println! o Display de 1.

Isso deve ser tão simples quanto o seguinte (eu acho).

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

Então, para erros, podemos ter:

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

onde a implementação é a mesma da RFC apenas com "{:?}" para usar
um formato Debug .

Como mencionado anteriormente, as pessoas que precisam de mais controle sobre a saída podem simplesmente
escrever:

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

Embora isso seja indecidível com nosso compilador atual, eu acho, já que
veria implementações conflitantes... Então fazemos o que @nikomatsakis sugere
e escreva:

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

Eu sei que isso é parcialmente reafirmar as coisas que foram ditas, mas pensei em apresentar minha visão por completo, mostrando que uma solução mais geral para exibir valores de retorno, não apenas resultados. Parece que muito deste comentário está discutindo quais implementações de Termination nós enviamos por padrão. Além disso, este comentário está em desacordo com a implementação como impl Termination for bool conforme descrito no RFC. Eu pessoalmente acho que códigos de saída diferentes de zero devem ser tratados exclusivamente por Results ou tipos personalizados que implementam Termination .

É uma questão interessante de como lidar com ? em Option tipos em main já que eles não têm uma implementação para Display .

TL;DR: Estou bem com Debug .

Explicação mais detalhada:
Passei algum tempo pensando sobre isso ontem e hoje com o objetivo de descobrir suposições implícitas que tenho ou estou fazendo para ajudar a chegar à melhor resposta possível.

Suposição chave: De todos os vários tipos de aplicativos que se pode escrever em Rust, acho que o aplicativo de console deve se beneficiar mais/ser mais impactado por essa decisão. Meu pensamento é que, ao escrever uma biblioteca, um título de jogo AAA, um IDE ou um sistema de controle proprietário, provavelmente não se esperaria que um traço de terminação principal padrão atendesse à necessidade pronta para uso (na verdade, pode-se nem ter um a Principal). Portanto, minha tendência para Display como padrão vem do que esperamos ver ao usar um pequeno aplicativo de linha de comando - por exemplo:

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

A maioria de nós não espera nenhum tipo de ajuda de depuração, apenas um indicador sucinto do que deu errado. Estou simplesmente defendendo isso como uma posição padrão.

Quando penso em escrever um Terminate impl para obter uma saída simples como esta, percebo que o ? do recurso principal não é tão diferente da ferrugem estável hoje (em termos de quantidade de código escrito ), onde um Result -aware "inner_main()" é frequentemente criado para lidar com E .

Com esta suposição em mente, como um exercício de pensamento, tentei determinar se eu sentia fortemente que a preponderância de implementações de estilo " inner_main() " existentes hoje eram de um sabor Display mais casual (sobre um sabor mais técnico de Debug ). Meu pensamento era que isso seria uma indicação de como o recurso provavelmente será usado.

Não consegui me convencer de que esse é o caso. (Ou seja, não acho que exista atualmente um forte viés em relação a Display nas implementações existentes). De fato, ao examinar meus próprios repositórios que escrevi nos últimos 16 meses, também encontrei o suficiente de ambos os casos que não posso dizer que a implementação Display por padrão equivaleria a uma economia líquida.

Mantendo a suposição "principal beneficiário é o aplicativo cli", há um grande número de aplicativos de console que fornecem ajuda e informações de uso. Por exemplo:

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

The most similar command is
    log

Portanto, mesmo para aplicativos de console, é difícil identificar um "grupo prejudicado" usando Debug .

E, finalmente, eu ficaria mais feliz com um impl de Debug do que com o recurso retido por mais 6 meses também, então, egoisticamente, é isso :).

Então aqui está o meu processo de pensamento exposto em público. Para resumir, acho que Debug não será melhor nem pior do que Display e, como tal, deve funcionar bem como a implementação padrão.

Como muitos de vocês, tenho certeza, gostaria que houvesse uma implementação que me deixasse mais empolgado - como "SIM, É ISSO!!!", TBH. Mas talvez sejam apenas minhas expectativas sendo irreais... Talvez uma vez que tenhamos uma solução que funcione com failure reduzindo o clichê em meus projetos, ela crescerá em mim. :)

Observe que abri um PR para dar suporte ao traço terminatio em testes: #48143 (com base no trabalho de @bkchr ).

Tomei a liberdade de estender o traço Termination com um método para processar o resultado do teste. Isso simplificou a implementação, mas também faz sentido, pois as falhas de teste podem exigir uma saída mais detalhada do que as falhas executáveis.

Termination deve ser renomeado para Terminate após nossa preferência geral por verbos para traços em libstd.

@withoutboats Acho que em algum momento houve uma discussão de que os traços verbais são principalmente aqueles que têm um único método com o mesmo nome do traço. De qualquer forma, posso flutuar novamente minha própria sugestão de bikeshed, Exit ?

Bikeshedding gratuito: esta é uma característica de método único. Se quisermos dar a eles o mesmo nome, talvez ToExitCode / to_exit_code ?

Estabilizar o retorno Result pode ser feito independentemente da estabilização do traço, certo?

Para mim, isso parece muito com ? onde a maior parte do valor vem do recurso de linguagem, e podemos demorar para descobrir o traço. A discussão do RFC até me fez pensar se o trait _ever_ precisa ser estabilizado, já que colocar o código em um inner_main parecia mais fácil do que uma implementação de trait para isso...

Sim, não precisamos estabilizar o trait - embora seja útil em stable para coisas como frameworks, que não podem necessariamente depender tanto de inner_main .

@SimonSapin Acho que To se refere a uma conversão de tipo, e isso não acontece. Mas poderíamos nomear o método terminate (também não acho que essa restrição sobre quando nomear verbos de traços se mantenha. Try é um contra-exemplo óbvio.)

Propus que estabilizemos fn main() -> T onde T não é unidade. Isso deixa muitos detalhes – particularmente o nome/localização/detalhes da característica – não estabilizados, mas algumas coisas são fixas. Detalhes aqui:

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

Por favor, dê seu feedback!

terminate parece ser mais descritivo do que report . Nós sempre terminate , mas podemos omitir report ing.

Mas ao contrário de std::process::exit por exemplo, este método não termina nada. Ele converte apenas o valor de retorno de main() em um código de saída (depois de imprimir opcionalmente Result::Err em stderr).

Mais um voto para Exit . Eu gosto que seja breve, bastante descritivo e consistente com o conceito tradicional de códigos de saída/status de saída.

Eu definitivamente preferiria Exit a Terminate , já que estamos saindo graciosamente ao retornar do main, em vez de encerrar repentinamente onde estamos porque algo deu muito errado.

Eu adicionei https://github.com/rust-lang/rust/issues/48854 para propor testes de unidade estabilizadores que retornam resultados.

Oh hey, eu encontrei o lugar certo para falar sobre isso.

Usando ? em doctests

A maneira como os doctests funcionam atualmente é algo assim:

  • rustdoc verifica o doctest para saber se ele declara um fn main

    • (atualmente, ele apenas faz uma pesquisa de texto linha por linha para "fn main" que não está após um // )

  • Se fn main foi encontrado, não tocará no que já está lá
  • Se fn main não for encontrado, ele envolverá a maioria* do doctest em um fn main() { } básico

    • *Versão completa: extrairá as declarações #![inner_attributes] e extern crate e as colocará fora da função principal gerada, mas todo o resto vai para dentro.

  • Se o doctest não incluir nenhuma instrução de grade externa (e a grade que está sendo documentada não for chamada std ), então o rustdoc também inserirá uma instrução extern crate my_crate; logo antes da função main gerada.
  • O rustdoc então compila e executa o resultado final como um binário autônomo, como parte do conjunto de testes.

(Deixei alguns detalhes de fora, mas convenientemente fiz um artigo completo aqui.)

Então, para usar ? perfeitamente em um doctest, a parte que precisa mudar é a parte em que adiciona fn main() { your_code_here(); } Declarar seu próprio fn main() -> Result<(), Error> funcionará assim que você puder que no código regular - rustdoc nem precisa ser modificado lá. No entanto, para fazê-lo funcionar sem declarar main manualmente, será necessário um pequeno ajuste. Não tendo seguido de perto esse recurso, não tenho certeza se existe uma solução de tamanho único. fn main() -> impl Termination é possível?

A terminação fn main() -> impl é possível?

Em um sentido superficial, sim: https://play.rust-lang.org/?gist=8e353379f77a546d152c9113414a88f7&version=nightly

Infelizmente, acho que -> impl Trait é fundamentalmente problemático com ? devido à conversão de erro embutida, que precisa do contexto de inferência para informar qual tipo usar: https://play.rust- lang.org/?gist=23410fa4fa684710bc75e16f0714ec4b&version=nightly

Pessoalmente, eu estava imaginando ? -in-doctests trabalhando através de algo como https://github.com/rust-lang/rfcs/pull/2107 como fn main() -> Result<(), Box<Debug>> catch { your_code_here(); } (usando sintaxe de https:// github.com/rust-lang/rust/issues/41414#issuecomment-373985777).

A versão impl Trait é legal, no entanto, e pode funcionar se houver algum tipo de suporte "prefira o mesmo tipo se não restrito" que o rustc poderia usar internamente no ? desugar, mas minha lembrança é que a ideia de um recurso como esse tende a fazer as pessoas que entendem como as coisas funcionam recuarem horrorizadas :sweat_smile: Mas talvez seja uma coisa interna que só se aplique se a saída for impl Trait seria viável ...

Ooh, essas são preocupações muito reais. Eu tinha esquecido como isso atrapalharia a inferência de tipos. Se o bloco catch iniciou o Ok-wrapping como esse problema vinculado, isso parece um caminho muito mais fácil (e muito mais rápido, em termos de estabilização).

A única coisa que me pergunto é como isso será afetado pela transição de edição. A sintaxe catch não está mudando na época de 2018? O Rustdoc provavelmente desejará compilar doctests na mesma edição que a biblioteca que está executando, portanto, seria necessário diferenciar as sintaxes com base no sinalizador de época que recebeu.

Estou preocupado que isso agora esteja estabilizado, mas casos simples aparentemente ainda 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

Em que ponto dizemos "isso é muito bugado e devemos desestabilizar"? Parece que se isso atingisse o canal estável em sua forma atual, causaria muita confusão.

@frewsxcv Acho que os problemas foram corrigidos agora, certo?

@nikomatsakis o problema que levantei em https://github.com/rust-lang/rust/issues/48389 foi resolvido em 1.26-beta, então sim da minha perspectiva.

Sim, o ICE com o qual eu estava preocupado está consertado agora!

Desculpas por entrar em contato em um ponto em que provavelmente é tarde demais para fazer algo sobre isso, mas queria deixar meu feedback aqui, caso houvesse. Eu li a maior parte deste tópico, então falo com esse contexto em mente. No entanto, este tópico é longo, então se parece que eu esqueci de algo, então eu provavelmente o fiz e eu gostaria de tê-lo apontado para mim. :-)

TL;DR - Acho que mostrar a mensagem Debug de um erro foi um erro, e que uma escolha melhor seria usar a mensagem Display de um erro.

No centro da minha crença é que, como alguém que _rotineiramente constrói programas CLI em Rust_, eu não consigo me lembrar de me importar muito com o que é a mensagem Debug de um Error . Ou seja, o Debug de um erro é, por design, para desenvolvedores, não para usuários finais. Quando você cria um programa CLI, sua interface é fundamentalmente destinada a ser lida pelos usuários finais, então uma mensagem Debug tem muito pouca utilidade aqui. Ou seja, se qualquer programa CLI que enviei para usuários finais mostrasse a representação de depuração de um valor Rust em operação normal, então eu consideraria que um bug foi corrigido. Eu geralmente acho que isso deve ser verdade para todos os programas CLI escritos em Rust, embora eu entenda que pode ser um ponto em que pessoas razoáveis ​​podem discordar. Com isso dito, uma implicação um tanto surpreendente da minha opinião é que efetivamente estabilizamos um recurso em que seu modo de operação padrão inicia com um bug (novamente, IMO) que deve ser corrigido.

Ao mostrar a representação Debug de um erro por padrão, também acho que estamos incentivando a má prática. Em particular, é muito comum durante a escrita de um programa Rust CLI observar que mesmo o Display impl de um erro não é bom o suficiente para ser consumido pelos usuários finais, e que um trabalho real deve ser feito para consertá-lo. Um exemplo concreto disso é io::Error . Mostrar um io::Error sem um caminho de arquivo correspondente (assumindo que veio da leitura/escrita/abertura/criação de um arquivo) é basicamente um bug, porque é difícil para um usuário final fazer qualquer coisa com ele. Ao optar por mostrar a representação Debug de um erro por padrão, tornamos mais difícil que esses tipos de bugs sejam descobertos por pessoas que criam programas CLI. (Além disso, o Debug de um io::Error é muito menos útil do que o Display , mas isso por si só não é um grande ponto de dor na minha experiência. )

Finalmente, para completar meu argumento, também tenho dificuldade em visualizar as circunstâncias sob as quais eu usaria ?-in-main mesmo em exemplos. Ou seja, tenho tentado escrever exemplos que correspondam aos programas do mundo real o mais próximo possível, e isso geralmente envolve escrever coisas assim:

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);
    }
}

À primeira vista, seria _lovely_ substituir isso por ?-in-main , mas não posso, porque não mostrará o Display de um erro. Ou seja, ao escrever um programa CLI real, usarei de fato a abordagem acima, portanto, se quiser que meus exemplos reflitam a realidade, acho que devo mostrar o que faço em programas reais e não usar atalhos (de forma razoável ). Na verdade, acho que esse tipo de coisa é realmente importante, e um efeito colateral disso historicamente foi que mostrou às pessoas como escrever código Rust idiomático sem espalhar unwrap em todos os lugares. Mas se eu voltar a usar ?-in-main em meus exemplos, acabei de renegar meu objetivo: agora estou configurando pessoas que podem não saber nada para escrever programas que, por padrão, emitem muito mensagens de erro inúteis.

O padrão de "emitir uma mensagem de erro e sair com um código de erro apropriado" é realmente usado em programas polidos. Por exemplo, se ?-in-main usasse Display , então eu poderia mesclar as funções main e run no ripgrep hoje:

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

Claro, eu poderia usar ?-in-main no futuro fornecendo meu próprio impl para o traço Termination assim que estabilizar, mas por que eu me incomodaria em fazer isso se eu pudesse escrever o main função que eu tenho? E mesmo assim, isso não ajuda no meu enigma ao escrever exemplos. Eu precisaria incluir esse impl nos exemplos para torná-los realidade, e nesse ponto, eu poderia ficar com os exemplos que tenho hoje (usando um main e try_main ).

Pelo que parece, parece que consertar isso seria uma mudança radical. Ou seja, este código compila no Rust estável hoje:

#[derive(Debug)]
struct OnlyDebug;

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

Acho que mudar para Display quebraria esse código. Mas não tenho certeza! Se esta é realmente uma questão de navio já partiu, então eu entendo e não há muito uso em aprofundar o ponto, mas eu me sinto forte o suficiente sobre isso pelo menos para dizer algo e ver se não consigo convencer os outros e ver se houver algo que possa ser feito para corrigi-lo. (Também é bem possível que eu esteja exagerando aqui, mas até agora algumas pessoas me perguntaram "por que você não está usando ?-in-main ?" em meus exemplos de CSV, e minha resposta foi basicamente, "Não vejo como seria viável fazer isso." Talvez esse não fosse um problema que deveria ser resolvido por ?-in-main , mas algumas pessoas certamente tiveram essa impressão. Com sua implementação atual , eu poderia vê-lo sendo útil em testes de documentos e testes de unidade, mas tenho dificuldade em pensar em outras situações em que eu o usaria.)

Concordo que exibir Display sobre Debug é melhor para aplicativos CLI. Eu não concordo que deveria ser Display em vez de Debug , porque isso limita drasticamente quais erros podem realmente ser ? -ed e anula o propósito de ?-in-main . Até onde posso dizer (pode estar totalmente errado, pois não tenho tempo para compilar stdlib e não sei se a especialização cobre isso) não há motivo para não podermos adicionar o seguinte impl ao Termination. Isso forneceria uma maneira ininterrupta de usar Display quando estiver disponível e voltar para Debug quando não estiver.

#[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()
    }
}

Finalmente, para completar meu argumento, também tenho dificuldade em visualizar as circunstâncias sob as quais eu usaria ?-in-main mesmo em exemplos.

Eu tenho que concordar com @BurntSushi em geral para programas CLI reais, mas para scripts aleatórios e ferramentas internas que apenas eu pretendo usar, isso é realmente conveniente. Além disso, é muito conveniente para protótipos e brinquedos. Nós sempre poderíamos desencorajar realmente usá-lo no código de produção, certo?

Parte do objetivo com ?-in-main é evitar código não idiomático em exemplos de código, ou seja, unwraps. Mas se o próprio ?-in-main torna-se unidiomático, isso prejudica todo o recurso. Eu gostaria fortemente de evitar qualquer resultado que nos levasse a desencorajar o uso, em código de produção ou de outra forma.

Eu tenho tentado usar esse recurso no main e achei muito frustrante. Os impls existentes da característica de terminação simplesmente não me permitem "acumular" convenientemente vários tipos de erros - por exemplo, não posso usar failure::Fail, porque não implementa Error; não consigo usar a caixa, mesma razão. Acho que devemos priorizar a mudança para Debug. =)

Se usarmos Display , podemos introduzir um tipo rápido e sujo como MainError para acumular vários tipos de erros:

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(),
        }
    }
}

Isso permitirá algo como abaixo semelhante a Box<Error> :

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

A discussão anterior de Display vs Debug está na parte oculta aqui, começando em https://github.com/rust-lang/rust/issues/43301#issuecomment -362020946.

@BurntSushi , concordo com você. Se isso não puder ser corrigido, pode haver uma solução alternativa:

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)
    }
}

Se isso estivesse em std::fmt , poderíamos usar algo assim:

use std::{fmt, io};

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

Ainda não tenho tempo para contribuir mais com isso no futuro próximo, mas também concordo que ? em main deve ser usado em programas CLI sérios, e havia recursos no rascunho original do RFC que se destinavam a facilitar isso. Eles foram descartados em nome do incrementalismo, mas talvez devam ser revisitados.

Acho que não há problema em corrigir isso de maneira incompatível com versões anteriores, se for feito em breve, antes que muito código esteja usando-o no estável.

Como alguém que escreve muitos programas CLI em Rust e é responsável pelo estilo Rust no trabalho, eu concordo com @burntsushi aqui. Eu ficaria feliz em usar a versão desse recurso especificada na RFC.

Mas eu considero mostrar uma implementação Debug para um usuário como um bug, e não muito melhor do que apenas chamar unwrap em qualquer lugar em um programa CLI. Portanto, mesmo no código de exemplo, não consigo imaginar usar essa versão do recurso. Isso é muito ruim, porque remover o clichê de main simplificaria o aprendizado para novos desenvolvedores de Rust no trabalho, e não precisaríamos mais explicar quick_main! ou o equivalente.

Praticamente as únicas circunstâncias em que posso imaginar usar isso seriam eliminar unwrap dos doctests. Mas eu não sei se isso é suportado ainda.

Se usarmos Display , podemos introduzir um tipo rápido e sujo como MainError para acumular vários tipos de erros:

Pessoalmente, esse tipo de solução adicionaria complexidade suficiente para que seja mais fácil evitar ? -in- main completamente.

@Aaronepower :

Até onde posso dizer (pode estar totalmente errado, pois não tenho tempo para compilar stdlib e não sei se a especialização cobre isso) não há motivo para não podermos adicionar o seguinte impl ao Termination. Isso forneceria uma maneira ininterrupta de usar o Display quando estiver disponível e retornar ao Debug quando não estiver.

Se isso me permitisse escrever fn main() -> Result<(), failure::Error> e obter um erro legal e legível por humanos usando Display , isso definitivamente satisfaria minhas principais preocupações. No entanto, suponho que isso ainda deixaria a questão de exibir opcionalmente o backtrace em failure::Error quando RUST_BACKTRACE=1 estiver definido - mas isso pode estar fora do escopo, de qualquer maneira, ou pelo menos um problema que deveria ser ocupado com failure .

Isso foi estabilizado há algum tempo e não acho que possamos realmente mudar o comportamento de Result . Pessoalmente, continuo muito feliz com o comportamento atual para os casos de uso que costumo fazer - scripts rápidos e sujos e similares - mas concordo que scripts mais complexos não vão querer isso. No entanto, no momento da discussão, também discutimos várias maneiras de controlar esse comportamento (não consigo encontrar esses comentários agora porque o github está escondendo coisas de mim).

Por exemplo, você pode definir seu próprio "wrapper" para o tipo de erro e implementar From para ele:

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

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

Agora você pode escrever algo assim, o que significa que você pode usar ? em main :

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

Talvez esse tipo deva fazer parte do quick-cli ou algo assim?

@nikomatsakis Sim, eu entendo totalmente essa solução alternativa, mas sinto que isso anula o propósito da concisão de usar ?-in-main . Eu sinto que "é possível usar ?-in-main usando esta solução alternativa" prejudica ?-in-main si, infelizmente. Por exemplo, não vou escrever sua solução alternativa em exemplos sucintos e também não vou impor uma dependência de quicli para cada exemplo que escrever. Eu certamente não vou usá-lo para programas rápidos também, porque eu quero a saída Display de um erro em basicamente todo programa CLI que eu escrevo. Minha opinião é que a saída de depuração de um erro é um bug nos programas CLI que você coloca na frente dos usuários.

A alternativa para ?-in-main é uma função adicional de ~4 linhas. Portanto, se a solução alternativa para corrigir ?-in-main para usar Display for muito mais do que isso, pessoalmente não vejo muita razão para usá-lo (fora de testes de documentos ou testes de unidade, como Mencionado acima).

Isso é algo que poderia ser mudado na edição?

@BurntSushi Como você se sentiria sobre a solução alternativa estar em std , então você só teria que escrever uma declaração de tipo de retorno um pouco mais longa em main() ?

@Kixunil Meu sentimento inicial é que pode ser palatável. Embora não tenha pensado muito nisso.

@BurntSushi

A alternativa para ?-in-main é uma função adicional de ~4 linhas.

Em última análise, o comportamento é estável, e não acho que possamos mudá-lo de forma realista neste momento. (Quero dizer, não é uma violação de solidez ou algo assim.)

Dito isso, ainda acho a configuração atual muito boa e preferível a Display , mas acho que é uma questão de como você deseja que o "padrão" seja - ou seja, qualquer configuração pode ser feita para funcionar como o outros através de vários newtypes. Portanto, optamos por favorecer scripts "rápidos e sujos", que desejam Debug , ou produtos finais polidos. Eu costumo pensar que um produto final polido é onde eu posso pagar uma importação adicional prontamente o suficiente, e também onde eu gostaria de escolher entre muitos formatos possíveis diferentes (por exemplo, eu quero apenas o erro, ou eu quero incluir argv [0], etc).

Para ser mais concreto, imagino que ficaria assim:

use failure::format::JustError;

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

ou, para obter um formato diferente, talvez eu faça isso:

use failure::format::ProgramNameAndError;

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

Ambos parecem razoavelmente bons em comparação com a função de 4 linhas que você tinha que escrever antes:

use std::sys;

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

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

Estou abordando esse tópico já que, para qualidade de produção, adicionar o que significa essencialmente um tipo de erro personalizado para a saída parece uma boa direção para empurrar as pessoas de qualquer maneira. Isso parece paralelo como panic! , por exemplo, fornece mensagens de qualidade de depuração por padrão.

@nikomatsakis Essa é talvez uma visão melhor a longo prazo, e acho seu resultado final apetitoso. Seria bom se esses tipos de erro terminassem em std algum dia para que pudessem ser usados ​​em exemplos com o mínimo de sobrecarga possível. :-)

Nota do processo: tentei enquadrar meu feedback inicial aqui com "há algo que pode ser feito" em vez de "vamos mudar algo que já se estabilizou", em um esforço para focar no objetivo de alto nível em vez de pedir algo que certamente não pode fazer. :-)

Bem, não demorou muito: https://crates.io/crates/exitfailure 😆

Eu estou querendo saber quantas pessoas são surpreendidas usando Debug traço versus Display traço. Tanto o rascunho da discussão inicial quanto o RFC final estão usando Display . Isso é semelhante ao incidente universal do traço imp, em que as pessoas pensavam que estavam recebendo uma coisa, mas só sabiam que estavam recebendo outra depois que ela se estabilizava. Considerando também que muitas pessoas que estão surpresas não usarão esse recurso, acho que não haverá uma grande barra invertida se mudarmos isso na próxima edição.

@WiSaGaN Os detalhes de um RFC podem e irão mudar de tempos em tempos. Isso é esperado e saudável. RFCs são documentos de design e não uma representação perfeita do que é e sempre será. A descoberta de informações é difícil e a culpa é minha por não prestar mais atenção. Espero que possamos evitar religar a estabilidade desse recurso e, em vez disso, nos concentrar nos objetivos de nível mais alto sobre os quais podemos agir de maneira viável.

@nikomatsakis O principal problema que vejo em favorecer casos rápidos e sujos é que já existe uma maneira de resolvê-los: .unwrap() . Então, dessa perspectiva, ? é apenas outra maneira de escrever coisas rápidas e sujas, mas não há uma maneira igualmente fácil de escrever coisas polidas.

É claro que o navio partiu, então estamos ferrados. Eu adoraria ver isso mudado na próxima edição, se possível.

@Kixunil , unwrap não é realmente uma boa maneira de resolver as coisas, pois o título deste tópico afirma que gostaríamos de poder usar o açúcar sintático ? em main . Eu posso entender de onde você está vindo (na verdade, foi parte da minha hesitação inicial com o uso de ? para manipulação de opções), mas com a inclusão de ? no idioma este problema é um muito bom usabilidade ganhar IMHO. Se você precisar de uma saída melhor, existem opções para você. Você não libera coisas para os usuários sem testá-las primeiro, e a descoberta de que você precisa de um tipo personalizado para a saída de main será uma realização bastante rápida.

Quanto ao "por que" gostaríamos de ? em main , considere o quão estranho é agora. Você pode usá-lo praticamente em qualquer outro lugar (desde que você tenha controle do tipo de retorno). A função main acaba se sentindo meio especial, o que não deveria.

.unwrap() é uma solução rápida e suja que não compõe, então você não pode fatorar o código dentro e fora de main() enquanto esboça um programa.

Em comparação, ? é uma solução rápida e suja que compõe, e torná-la não rápida e suja é uma questão de colocar o tipo de retorno correto em main() , não de modificar o próprio código.

@Screwtapello ah, agora faz sentido para mim. Obrigado!

Eu só quero expressar que eu pensei que o objetivo disso era trazer ? em main para todos os aplicativos sem ter que recorrer a wrappers adicionais... ver muito benefício nisso e continuará aderindo a .unwrap() , infelizmente.

@oblitum Não é apenas para testes. Não é apenas (por padrão) que vai usar o formato Display . Esta pode ou não ser a saída que você está procurando, mas quase definitivamente será melhor que a saída de .unwrap . Com isso dito, pode tornar seu código "mais agradável" usar ? . Em qualquer caso, ainda é possível personalizar a saída de erros ? em main como @nikomatsakis detalhou acima.

Seria possível introduzir um novo tipo no stdlib para a vitrine? Algo como:

#[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> {}
// ...

Isso deve permitir que os usuários escrevam:

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

As principais motivações aqui são:

  • A exibição é usada quando um erro é retornado
  • Os usuários não precisam fazer nenhum agrupamento extra de seus tipos de erro ou resultado; eles podem simplesmente usar ? em sua função principal.

Acompanhamento: Ocorre-me que a semântica de ? e From pode não permitir que isso funcione tão implicitamente quanto eu gostaria. Eu sei que ? converterá entre os tipos de erro via From , mas não sei se ele faz a mesma coisa para diferentes implementadores de Try . Ou seja, ele converterá de Result<?, io::Error> para Result<?, FromIoError> , mas não necessariamente de Result<?, Err> para DisplayResult<Result<?, Err>> . Basicamente, estou procurando algo assim para funcionar:

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

Supondo que isso não funcione, talvez isso possa ser feito um sinalizador de compilação condicional simples em Cargo.toml?

@Lucretiel : você parece supor que os programadores querem apenas imprimir uma mensagem de saída no console. Isso não vale para aplicativos de desktop não técnicos, daemons com encanamento de log, etc. Prefiro que não adicionemos coisas 'comumente úteis' à biblioteca padrão sem evidências convincentes (por exemplo, números) de que deveriam estar lá.

Estou assumindo que os programadores que estão retornando um Result<(), E> where E: Error de main estão interessados ​​em imprimir uma mensagem de erro no console, sim. Mesmo o comportamento atual, que é imprimir via Debug , é "imprimir uma mensagem de saída no console".

Parece haver um amplo acordo neste segmento de que algo deve ser impresso em stderr; a principal decisão a ser tomada é "Deve imprimir com "{}" ou "{:?}" , ou qualquer outra coisa, e quão configurável deve ser, se for o caso".

@Lucretiel , para mim, o valor de retornar um Result está controlando bem o status de saída. A impressão de algo que não seria melhor seria deixada para o manipulador de pânico? Não há apenas a questão de imprimir algo no stderr, também o que imprimir (backtrace, mensagem de erro, etc?). Consulte https://github.com/rust-lang/rust/issues/43301#issuecomment -389099936, esp. a última parte.

Eu usei isso recentemente e desejei que ele chamasse Display . Tenho certeza que fizemos a ligação errada aqui.

Uma falha de processo que noto é que a decisão foi tomada pela equipe lang (principalmente Niko e eu) em vez da equipe libs, mas o código em questão vive inteiramente em std. Possivelmente libs teria tomado uma decisão melhor.

@withoutboats é tarde demais para mudar? Percebo que o recurso ainda está marcado como nightly-only/experimental nos documentos , mas é possível que eu não entenda parte da granularidade por trás do processo de estabilização.

Eu também gostaria de ver essa mudança e me arrependo de não ter sido um defensor mais forte dela quando a equipe me pediu para mudar a implementação de Display para Debug .

@Lucretiel Termination O traço é noturno, mas o recurso em si (retornando implementações de $# Termination de main /tests) já está estável.

Existe um ticket ou um cronograma para estabilizar o nome da característica?

@xfix Isso implicaria que é possível alterar a implementação, certo? O comportamento não muda ( Termination retornado de main ), mas a característica em si mudaria de:

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

para:

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

é possível alterar a implementação, certo?

Não sem quebrar a compatibilidade com versões anteriores. Este código compila no Rust estável de hoje:

#[derive(Debug)]
struct X;

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

Com a mudança proposta para exigir Display , ele pararia de compilar.

Como já foi mencionado , é possível que algo como especialização possa ajudar, mas meu entendimento atual de especialização é que só seria útil para os tipos que implementam Display e Debug (ou seja, de tal forma que haja uma implementação que é mais especial).

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,
        }
    }
}

Parque infantil

Isso pode ser suficiente para cobrir todos os casos comuns, mas expõe publicamente a especialização.

Ah, entendo o que você quer dizer sobre ferrugem estável. Por algum motivo, eu não sabia que isso estava disponível no stable.

Isso é algo que pode ser trocado em tempo de compilação com um sinalizador de carga? Da mesma forma que outras coisas são comutáveis, como panic = "abort" .

Pode ser? Vi ser viável ter um comportamento diferente para ? em main em outra edição do Rust. Provavelmente não 2018, no entanto, talvez 2021?

Caixas diferentes no mesmo programa podem estar em edições diferentes, mas usam o mesmo std , então as características disponíveis em std e seus impls precisam ser os mesmos em todas as edições. O que poderíamos fazer em teoria (não estou defendendo isso) é ter dois traços, Termination e Termination2 , e exigir o tipo de retorno de main() para implementar um ou o outro dependendo da edição do engradado que define main() .

Acho que não precisamos depreciar nada. Vou repetir o sentimento de @Aaronepower de que limitar apenas os tipos de exibição pode servir apenas para desagradar os usuários de script rápido (derivar Debug é trivial, implementar Display não é) da mesma maneira que a situação atual desagrada os usuários de produção. Mesmo se pudéssemos, eu não acho que isso seria um benefício. Acho que a abordagem de especialização apresentada pelo @shepmaster é promissora, pois criaria o seguinte comportamento:

  1. Um tipo que tem Debug mas não Display terá sua saída de Debug impressa por padrão
  2. Um tipo que tem Debug e Display terá sua saída Display impressa por padrão
  3. Um tipo que tem Display mas não Debug resulta em um erro do compilador

Alguém pode se opor ao ponto 2, no caso de você ter um tipo com Display, mas quiser que ele imprima Debug por qualquer motivo; Acho que esse caso seria abordado pela proposta de Niko de vários tipos de erro de formato de saída pré-projetados (que provavelmente valem a pena explorar, mesmo se seguirmos a rota de especialização).

Quanto ao ponto 3, é um pouco estranho (talvez devêssemos ter feito trait Display: Debug de volta em 1.0?), mas se alguém se deu ao trabalho de escrever um Display impl, então não é pedir muito que eles dêem um tapa uma única derivação (que, eu suspeito, eles provavelmente têm de qualquer maneira ... existe alguém por aí que se opõe, em princípio, a ter Debug em seu tipo de exibição?).

limitar apenas os tipos de exibição pode servir apenas para desagradar os usuários de script rápido (derivar depuração é trivial, implementar exibição não é)

Um caso em que Display é preferível a Debug em scripts rápidos está usando &'static str ou String como o tipo de erro. Se você tiver um tipo de erro personalizado para aplicar #[derive(Debug)] você já está gastando alguma energia não trivial no tratamento de erros.

@SimonSapin Eu não diria que é preferível em um ambiente de script rápido, ou melhor, não é preferível o suficiente para estar em uma posição em que eu quisesse que a String fosse impressa no formato Display sem querer fazer o esforço para ter erros formatados corretamente, para mim Debug é o preferido porque o formato do erro quando é String não é bem formado, então ter Err("…") ao redor me permite escolher visualmente o erro de qualquer outro saída que pode ter sido emitida.

FWIW o que está formatado não é o Result<_, E> , apenas o E dentro. Portanto, você não verá Err( na saída. No entanto, o código atual adiciona um prefixo Error: , que presumivelmente permaneceria se Display fosse usado. Além de não imprimir aspas, Display também não escaparia com barra invertida do conteúdo da string, o que é desejável IMO quando se trata de mostrar uma mensagem (de erro) aos usuários.

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

Acho que, dada a situação, a melhor coisa que podemos fazer é a especialização para tipos que implementam Display + Debug para imprimir o Display impl. É lamentável que tipos que não implementam Debug não possam ser usados ​​como erros (até que suportemos impls de interseção), mas é o melhor que podemos fazer.

A questão principal é se aquele imp se enquadra ou não na nossa política atual de imps especializados em std. Geralmente temos uma regra de que só usamos especialização em std onde não fornecemos uma garantia estável de que o comportamento não mudará mais tarde (porque a especialização em si é um recurso instável). Estamos confortáveis ​​em fornecer este impl agora sabendo que é possível que algum dia esses tipos voltem a imprimir sua saída Debug se eventualmente removermos a especialização como um recurso?

Na verdade, acho que não podemos permitir essa especialização agora, porque poderia ser usada para introduzir insanidade. Este impl é exatamente o tipo de especialização que nos causa tantos problemas!

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(())
    }
}

Por causa da maneira como a especialização funciona atualmente, eu poderia chamar report em um Foo<'a, 'b> onde 'b não sobrevive 'a , e atualmente é inteiramente possível que o compilador irá selecionar o impl que chama a instância Display mesmo que os requisitos de tempo de vida não sejam atendidos, permitindo que o usuário estenda o tempo de vida de uma referência de forma inválida.

Eu não sei vocês, mas quando eu escrevo "scripts" rápidos, eu apenas unwrap() a merda de tudo. É 100 vezes melhor, porque tem backtraces que me fornecem informações contextuais. Sem ele, caso eu tenha let ... = File::open() mais de uma vez no meu código, já não consigo identificar qual falhou.

Eu não sei vocês, mas quando eu escrevo "scripts" rápidos, eu apenas unwrap() a merda de tudo. É 100 vezes melhor, porque tem backtraces que me fornecem informações contextuais. Sem ele, caso eu tenha let ... = File::open() mais de uma vez no meu código, já não consigo identificar qual falhou.

Na verdade, é por isso que me sinto tão fortemente em relação a Display , já que geralmente o que faço neste caso é fazer um map_err que anexa o nome do arquivo e outras informações contextuais relevantes. Geralmente, a saída de depuração é menos útil para rastrear o que realmente deu errado, na minha experiência.

@Kixunil : Alternativamente, eu preferiria uma estratégia de tratamento de erros mais elaborada e/ou registro e/ou depuração mais extensos.

Eu acho que "Implementar o RFC" pode ser marcado, certo? Pelo menos de acordo com isso ! :sorrir:

Perdoe-me se isso é completamente sem noção, mas você não poderia usar a especialização para relatar a cadeia de erros quando possível:

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
            }
        }
    }
}

Existe uma razão para esta parte da RFC não ter sido implementada?

Fizemos algumas mudanças nos conjuntos precisos de traits e imps a serem usados, mas não me lembro dos detalhes neste momento - e acredito que ainda restam alguns detalhes para serem estabilizados. Você pode querer verificar os relatórios de estabilização vinculados ao problema principal para obter mais detalhes.

Qual é o status desse recurso? Existe uma proposta de estabilização?

@GrayJack Este problema deve ser encerrado, pois ocorreu há algum tempo.

É verdade, e fiquei um pouco confuso, cheguei aqui do link no traço Termination e estava perguntando sobre isso, existe um problema adequado para termination_trait_lib ?

Esta questão deve ser encerrada

Este problema ainda está em aberto para acompanhar a estabilização de Termination .

Esta página foi útil?
0 / 5 - 0 avaliações