Rust: Rastreamento de problema para `ops :: Try` (recurso` try_trait`)

Criado em 31 mai. 2017  ·  99Comentários  ·  Fonte: rust-lang/rust

O traço Try de https://github.com/rust-lang/rfcs/pull/1859; implementado em PR https://github.com/rust-lang/rust/pull/42275.

Divida https://github.com/rust-lang/rust/issues/31436 para maior clareza (por https://github.com/rust-lang/rust/pull/42275#discussion_r119167966)

  • [] Estabilizar isso permitirá que as pessoas implementem Iterator::try_fold

    • [] Como parte da estabilização, reabra o PR # 62606 para documentar a implementação de try_fold para iteradores

    • [] Certifique-se de que as implementações padrão de outras coisas tenham o DAG de longo prazo desejado, uma vez que alterá-las é essencialmente impossível mais tarde. (Especificamente, seria bom ter fold implementado em termos de try_fold , para que ambos não precisem ser substituídos.)

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

Comentários muito úteis

Qual é o status atual deste recurso?

Todos 99 comentários

Algumas peças da bicicleta:

  • Temos uma motivação específica para chamar o tipo associado de Error vez de Err ? Chamá-lo de Err faria com que ele se alinhasse com Result : o outro já se chama Ok .

  • Temos uma motivação particular para ter métodos from_error e from_ok separados, em vez de um único from_result que seria mais simétrico com into_result que é o outro metade do traço?

( versão atualizada do link do playground

@glaebhoerl

  • Error vs Err foi discutido relacionado a TryFrom em https://github.com/rust-lang/rust/issues/33417#issuecomment -269108968 e https: // github.com/rust-lang/rust/pull/40281; Presumo que o nome foi escolhido aqui por razões semelhantes.
  • Eu acredito que eles são separados porque têm usos diferentes, e espero que seja raro alguém realmente ter um Result que está tentando transformar em T:Try . Eu prefiro Try::from_ok e Try::from_error a sempre chamar Try::from_result(Ok( e Try::from_result(Err( , e estou feliz em apenas implantar os dois métodos ao invés de escrever a correspondência. Talvez seja porque eu penso em into_result não como Into<Result> , mas como "foi aprovado ou reprovado?", Com o tipo específico sendo Result como um detalhe de implementação sem importância. (Não quero sugerir ou reabrir o "deve haver um novo tipo para valor de produção versus retorno antecipado, no entanto.) E para documentação, gosto que from_error fale sobre ? (ou eventualmente throw ), enquanto from_ok fala sobre empacotamento com sucesso (# 41414), ao invés de ter os dois no mesmo método.

Não tenho certeza se este é o fórum correto para este comentário, por favor, redirecione-me se não for: smiley : https://github.com/rust-lang/rfcs/pull/1859 e eu perdi o período de comentários; oops!


Eu queria saber se há um caso para dividir o traço Try ou remover o método into_result ; para mim atualmente Try é algo como a soma dos traços FromResult (contendo from_error e from_ok ) e IntoResult (contendo into_result ).

FromResult permite uma saída antecipada altamente ergonômica com o operador ? , que eu acho que é o caso de uso matador para o recurso. Acho que IntoResult já pode ser implementado perfeitamente com métodos por caso de uso ou como Into<Result> ; estou faltando alguns exemplos úteis?

Seguindo o RFC de traço Try , expr? poderia desugar como:

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

Os exemplos de motivação que considerei são Future e Option .

Futuro

Podemos implementar FromResult naturalmente para struct FutureResult: Future como:

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

Assumindo uma implementação razoável para ? que retornará de uma função com valor impl Future quando aplicada a Result::Err então posso escrever:

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

Isso é exatamente o que eu estava tentando implementar hoje cedo e Try acertou em cheio! Mas se tentarmos implementar o Try atual para Future , talvez não haja escolha canônica para into_result ; pode ser útil entrar em pânico, bloquear ou pesquisar uma vez, mas nada disso parece universalmente útil. Se não houvesse into_result em Try , posso implementar a saída antecipada como acima, e se precisar converter Future em Result (e daí para qualquer Try ) Posso convertê-lo com um método adequado (chame o método wait para bloquear, chame alguns poll_once() -> Result<T,E> , etc.).

Opção

Option é um caso semelhante. Implementamos from_ok , from_err naturalmente como no RFC de traço Try , e poderíamos converter Option<T> em Result<T, Missing> simplesmente com o.ok_or(Missing) ou um método de conveniência ok_or_missing .


Espero que ajude!

Talvez eu esteja muito atrasado para tudo isso, mas me ocorreu neste fim de semana que ? tem uma semântica bastante natural nos casos em que você gostaria de um curto-circuito em _sucesso_.

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

Mas, neste caso, a nomenclatura dos métodos de Try traits não se aplica.

No entanto, isso ampliaria o significado do operador ? .

Ao fazer o protótipo de try_fold para rayon , descobri que queria algo como Try::is_error(&self) para os métodos Consumer::full() . (Rayon tem isso porque os consumidores são basicamente iteradores no estilo push, mas ainda queremos curto-circuito nos erros.) Em vez disso, tive que armazenar os valores intermediários como Result<T::Ok, T::Err> para que pudesse chamar Result::is_err() .

A partir daí, também desejei que Try::from_result voltasse para T no final, mas um match mapeando para T::from_ok e T::from_error não é _muito_ ruim.

O traço Try pode fornecer um método from_result para ergonomia se from_ok e from_error forem necessários. Ou vice-versa. A partir dos exemplos dados, vejo um caso para oferecer ambos.

Como o PR # 42275 foi mesclado, isso significa que o problema foi resolvido?

@skade Observe que um tipo pode definir qualquer um que LoopState faz para alguns Iterator internos. Talvez isso fosse mais natural se Try falasse sobre "continuar ou quebrar" em vez de sucesso ou fracasso.

@cuviper A versão que acabei precisando era do tipo Ok ou do tipo Error -in-original. Espero que a destruição e reconstrução seja algo geral o suficiente para otimizar bem e um monte de métodos especiais sobre a característica não sejam necessários.

@ErichDonGubler Este é um problema de rastreamento, então não é resolvido até que o código correspondente esteja estável.

Relato de experiência:

Estou um pouco frustrado tentando colocar essa característica em prática. Várias vezes já fui tentado a definir minha própria variante em Result por qualquer motivo, mas cada vez acabei usando apenas Result no final, principalmente porque implementando Try era muito chato. Eu não estou totalmente certo se isso é uma coisa boa ou não, embora!

Exemplo:

No novo solver para o Chalk VM, eu queria ter um enum que indica o resultado da resolução de uma "vertente". Isso tinha quatro possibilidades:

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

Eu queria que ? , quando aplicado a este enum, desembrulhasse o "sucesso", mas propagasse todas as outras falhas para cima. No entanto, para implementar o traço Try , eu teria que definir um tipo de tipo "residual" que encapsula apenas os casos de erro:

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

Mas, uma vez que tenho esse tipo, posso também fazer StrandResult<T> um alias:

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

e foi isso que eu fiz.

Agora, isso não parece necessariamente pior do que ter um único enum - mas parece um pouco estranho. Normalmente, quando escrevo a documentação de uma função, por exemplo, não "agrupo" os resultados por "ok" e "erro", mas sim falo sobre as várias possibilidades como "iguais". Por exemplo:

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

Observe que eu não disse "retornamos Err(StrandFail::NoSolution) . Isso ocorre porque Err parece um artefato irritante que preciso adicionar.

(Por outro lado, a definição atual ajudaria os leitores a saber qual é o comportamento de ? sem consultar o Try impl.)

Eu acho que este resultado não é tão surpreendente: o atual Try impl força você a usar ? em coisas que são isomórficas a Result . Essa uniformidade não é acidental, mas, como resultado, torna-se irritante usar ? com coisas que não são basicamente "apenas Result ". (Por falar nisso, é basicamente o mesmo problema que dá origem a NoneError - a necessidade de definir artificialmente um tipo para representar a "falha" de um Option .)

Também observaria que, no caso de StrandFail , não quero particularmente a conversão de From::from que os resultados comuns obtêm, embora não esteja me prejudicando.

O tipo Try::Error associado já foi exposto / usado pelo usuário diretamente? Ou é apenas necessário como parte da remoção de ? ? Se for o último, não vejo nenhum problema real em apenas defini-lo "estruturalmente" - type Error = Option<Option<(Strand, Minimums)>> ou qualquer outra coisa. (Ter que descobrir o equivalente estrutural da "metade da falha" da definição de enum não é ótimo, mas parece menos irritante do que ter que reajustar toda a API voltada para o público.)

Eu também não sigo. Implementei com sucesso o Try para um tipo que tinha uma representação de erro interno e parecia natural ter Ok e Error do mesmo tipo. Você pode ver a implementação aqui: https://github.com/kevincox/ecl/blob/8ca7ad2bc4775c5cfc8eb9f4309b2666e5163e02/src/lib.rs#L298 -L308

Na verdade, parece que existe um padrão bastante simples para fazer este trabalho.

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

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

Se você quiser desvendar o sucesso, algo assim deve funcionar:

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

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

Definir um tipo estrutural é possível, mas parece muito chato. Eu concordo que posso usar Self . Parece um pouco estranho para mim, porém, que você pode usar ? para converter de StrandResult para Result<_, StrandResult> etc.

Excelente relato de experiência, @nikomatsakis! Também não estou satisfeito com Try (da outra direção).

TL / DR : Eu acho que FoldWhile acertou, e devemos dobrar a Break -vs- Continue interpretação de ? vez de falar sobre em termos de erros.

Mais tempo:

Eu continuo usando Err para algo mais próximo de "sucesso" do que de "erro" porque ? é muito conveniente.

Então, se nada mais, eu acho que a descrição que escrevi para Try está errada e não deveria falar sobre uma "dicotomia sucesso / falha", mas sim abstrair de "erros".

A outra coisa que estive pensando é que deveríamos considerar alguns impls ligeiramente malucos por Try . Por exemplo, Ordering: Try<Ok = (), Error = GreaterOrLess> , combinado com "funções de teste", pode permitir que cmp para struct Foo<T, U> { a: T, b: U } seja apenas

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

Ainda não sei se esse é o tipo bom de louco ou o mau: rindo: certamente tem um pouco de elegância, já que, uma vez que as coisas são diferentes, você sabe que são diferentes. E tentar atribuir "sucesso" ou "erro" a qualquer um dos lados disso não parece se encaixar bem.

Também observo que essa é outra instância em que a "conversão de erro" não é útil. Eu também nunca usei nos exemplos "Eu quero? Mas não é um erro" acima. E é certamente conhecido por causar tristeza de inferência, então eu me pergunto se é algo que deveria ser limitado apenas ao Resultado (ou potencialmente removido em favor de .map_err(Into::into) , mas isso provavelmente é inviável).

Edit: Ah, e tudo o que me faz pensar se talvez a resposta para "Eu continuo usando Result para meus erros em vez de implementar Try para um tipo meu" é "bom, isso é esperado".

Edição 2: não é diferente de https://github.com/rust-lang/rust/issues/42327#issuecomment -318923393 acima

Edição 3: parece que uma variante Continue também foi proposta em https://github.com/rust-lang/rfcs/pull/1859#issuecomment -273985250

Meus dois centavos:

Eu gosto da sugestão de @fluffysquirrels de dividir o traço em dois traços. Um para converter em um resultado e outro para converter em um resultado. Mas eu acho que devemos manter into_result ou equivalente como parte da remoção. E acho que neste ponto temos que fazer isso, já que o uso de Option como um Try estabilizou.

Também gosto da ideia de @scottmcm de usar nomes que sugerem interromper / continuar ao invés de erro / ok.

Para colocar o código específico aqui, gosto de como se lê:

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

É claro que não é tão bom quanto um loop reto, mas com "métodos try" seria quase:

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

Para efeito de comparação, acho a versão do "vocabulário do erro" realmente enganosa:

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

Podemos implementar Display para NoneError? Isso permitiria que a caixa de falha derivasse automaticamente From<NoneError> for failure::Error . Veja https://github.com/rust-lang-nursery/failure/issues/61
Deve ser uma mudança de 3 linhas, mas não tenho certeza sobre o processo de RFCs e tal.

@ cowang4 Eu gostaria de tentar evitar a ativação de qualquer Try e o desugar em algo que não precisava de NoneError ...

@scottmcm Ok. Eu vejo seu ponto. Eventualmente, gostaria de uma maneira limpa de aprimorar o padrão de retornar Err quando uma função de biblioteca retornar Nenhum. Talvez você conheça outro que não seja Try ? Exemplo:

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

Depois de encontrar esse recurso experimental e a caixa de failure , naturalmente gravitei para:

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

O que _quase_ funciona, exceto pela falta de impl Display for NoneError como mencionei antes.
Mas, se essa não for a sintaxe que gostaríamos de usar, talvez possa haver outra função / macro que simplifique o padrão:

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

@ cowang4 Eu acredito que funcionaria se você implementasse From<NoneError> para failure::Error , que usasse seu próprio tipo que implementasse Display .

No entanto, provavelmente é uma prática melhor usar opt.ok_or(_)? para que você possa dizer explicitamente qual deve ser o erro se a opção for Nenhum. Em seu exemplo, por exemplo, você pode querer um erro diferente se pb.file_stem for None do que se to_str() retornar None.

@tmccombs Tentei criar meu próprio tipo de erro, mas devo ter feito errado. Era assim:

#[macro_use] extern crate failure_derive;

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

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

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

E então tentei usar meu tipo de erro ...

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

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

Ok, acabei de perceber que ele quer saber como converter de outros tipos de erro para o meu erro, que posso escrever genericamente:

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

Não...

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

Ok, e quanto a std::error::Error ?

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

Isso também não funciona. Em parte porque está em conflito com meu From<NoneError>

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

O que é estranho porque pensei que NoneError não implementou std::error::Error . Quando comento meu impl From<NoneError> não genérico, recebo:

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

Tenho que escrever todos os From s manualmente. Eu pensei que a caixa de falha deveria derivá-los?

Talvez eu deva ficar com option.ok_or()

Tenho que escrever todos os Froms manualmente. Eu pensei que a caixa de falha deveria derivá-los?

Não acho que a caixa de falhas faça isso. Mas posso estar errado.

Ok, então eu reexamei a caixa de falha, e se estou lendo a documentação e as diferentes versões certas, ela foi projetada para sempre usar failure::Error como o tipo de erro em seus Result s, veja aqui . E ele implementa um traço impl Fail para a maioria dos tipos de erro:

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

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

E então um impl From para que possa Try / ? outros erros (como os de std) no tipo geral failure::Error .
rust impl<F: Fail> From<F> for ErrorImpl
https://github.com/rust-lang-nursery/failure/blob/d60e750fa0165e9c5779454f47a6ce5b3aa426a3/failure-1.X/src/error/error_impl.rs#L16

Mas, é só que, uma vez que o erro relevante para este problema de ferrugem, NoneError , é experimental, ele ainda não pode ser convertido automaticamente, porque não implementa o traço Display . E não queremos, porque isso confunde mais a linha entre Option s e Result s. Provavelmente tudo será retrabalhado e resolvido eventualmente, mas por enquanto, vou me limitar às técnicas sem açúcar que aprendi.

Obrigado a todos por ajudar. Estou aprendendo lentamente o Rust! :sorriso:

Vou me limitar às técnicas sem açúcar que aprendi.

: +1: Honestamente, acho que .ok_or(...)? continuará sendo o caminho a percorrer (ou .ok_or_else(|| ...)? , é claro). Mesmo se NoneError _had_ a Display impl, o que diria? "Algo não estava lá"? Isso não é um grande erro ...

Tentando chegar mais perto de uma proposta concreta ...

Estou começando a gostar da alternativa TrySuccess . Curiosamente, acho que _nenhum_, inclusive eu, gostou desse originalmente - nem mesmo está na versão final do RFC. Mas, felizmente, ele vive na história: https://github.com/rust-lang/rfcs/blob/f89568b1fe5db4d01c4668e0d334d4a5abb023d8/text/0000-try-trait.md#using -an-associated-type-for-the-success -valor

Parece-me que a maior objeção a isso foi a reclamação razoável de que todo um traço central para apenas um tipo associado ( trait TrySuccess { type Success; } ) era um exagero. Mas com catch try bloqueios de volta (https://github.com/rust-lang/rust/issues/41414#issuecomment-373985777) para fazer o empacotamento ok (como no RFC) , de repente tem um uso direto e importante: esse é o traço que controla essa embalagem. Juntamente com o objetivo " ? deve sempre produzir o mesmo tipo" que eu acho que era geralmente desejado, o traço parece aguentar melhor seu peso. Espantalho:

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

Esse tipo associado, como aquele que será retornado do operador ? , é também aquele que claramente precisa existir. Isso significa que não atingiu o aborrecimento "tinha que definir uma espécie de tipo 'residual'" que Niko articulou . E sendo () é razoável, até comum, então evita contorções semelhantes a NoneError .

Edit: Para a bicicleta, "return" pode ser uma boa palavra, já que isso é o que acontece quando você return de um método try (também conhecido como ok-wrapping). return também é o nome do operador mônada para isso, iiuc ...

Edição 2: Meus pensamentos sobre as outras características / métodos ainda não se estabilizaram, @tmccombs.

@scottmcm só para ficar claro, ou você está sugerindo as duas características a seguir?

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

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

E desugurar x? seria algo como:

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

e try { ...; expr} desugaria para algo como:

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

@scottmcm Eu também acho essa variante muito mais atraente quando considero ok-wrapping =)

Continuando com o próximo traço, acho que aquele para ? torna-se este (módulo massivo de bicicletas):

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

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

Justificativa variada:

  • Isso requer TryContinue para seu tipo de argumento, de modo que o tipo de uma expressão x? seja sempre o mesmo
  • Isso padroniza o parâmetro de tipo para Self para casos simples como try_fold que inspecionam e retornam o mesmo tipo, portanto, tudo bem com apenas T: Try .
  • Esta característica estende TryContinue porque se um tipo usado como um tipo de retorno permite ? em seu corpo, então ele também deve suportar ok-wrapping.
  • O novo enum resulta isomórfico, mas evita falar em termos de "erros" conforme discutido acima e, como resultado, acho que fornece um significado muito claro para cada uma das variantes.
  • O argumento para ? é o tipo genérico de modo que, como TryContinue , ele "produz um Self de algo"

Demonstração completa de prova de conceito, incluindo macros para try{} / ? e impls para Option , Result e Ordering : https : //play.rust-lang.org/? gist = 18663b73b6f35870d20fd172643a4f96 & version = stable (obrigado @nikomatsakis por fazer o original destes um ano atrás 🙂)

Desvantagens:

  • Ainda tem os parâmetros de tipo fantasma no Result impl que eu realmente não gosto

    • Mas talvez seja uma boa troca por não precisar de um tipo LessOrGreater extra no implemento Ordering .

    • E os parâmetros de tipo fantasma em impls são menos nojentos do que em tipos de qualquer maneira

  • Não fornece uma transformação From consistente

    • Mas isso pode ser bom, já que StrandResult não se importava de qualquer maneira, para algo como SearchResult seria estranho, pois o caminho de quebra é o caminho de sucesso e pode acabar ajudando a inferir os casos aninhados de ? qualquer maneira.

  • Não é tão óbvio qual seria a sintaxe de throw

    • O que perde a explicação de ? como Ok(x) => x, Err(r) => throw e.into() que eu realmente gostei

    • Mas também poderia deixar throw; ser a maneira de produzir None (por meio de algo como impl<T> Throw<()> for Option<T> ), que é muito melhor do que throw NoneError;

    • E separar isso pode ser bom de qualquer maneira, já que throw LessOrGreater::Less teria sido _realmente_ bobo.

Edit: Ping @glaebhoerl , cuja opinião eu gostaria sobre isso como um grande participante da RFC 1859.

Edição 2: Também cc @ colin-kiegel, para esta declaração de https://github.com/rust-lang/rfcs/pull/1859#issuecomment -287402652:

Eu me pergunto se a abordagem essencialista poderia adotar um pouco da elegância dos reducionistas sem sacrificar os objetivos acima.

Eu realmente gosto dessa proposta.

Não é tão óbvio qual seria a sintaxe de lançamento

Ignorando a bagagem da palavra-chave throw de outras línguas, "jogar" faz sentido, pois você está "jogando" o valor para um escopo mais alto. Python e scala também usam exceções para o fluxo de controle além dos casos de erro (embora, no caso do scala, você normalmente não usaria try / catch diretamente), portanto, há algum precedente para o uso de throw para caminhos bem-sucedidos.

@tmccombs

Ignorando a bagagem da palavra-chave throw de outras línguas, "jogar" faz sentido, pois você está "jogando" o valor para um escopo mais alto.

Embora venha com bagagem semelhante, suspeito que "aumentar" é uma opção melhor:

lançar (v): colocar ou fazer ir ou entrar em algum lugar, posição, condição, etc., como se lançando:

aumentar (v): mover para uma posição superior; erguer; elevar

Pode haver uma maneira de combinar o "aumento" com a lógica de ? , visto que o aumento também pode significar "receber". Algo como: Ok(v) => v, Err(e) => raise From::from(e) , onde raise imita o padrão combinado (por exemplo, dado um padrão Err(e) , é mágica sintática para ControlFlow::Break(Err(...)) ).

Eu suspeito que "aumentar" é um ajuste melhor

bom ponto

@scottmcm

Não é tão óbvio qual seria a sintaxe de lançamento

Existe um motivo pelo qual não podemos ter from_err(value: Other) ?

ATUALIZAÇÃO: Talvez eu esteja confuso sobre o papel de Other , na verdade. Tenho que estudar esse traço um pouco mais. =)

@nikomatsakis

Existe um motivo pelo qual não podemos ter from_err(value: Other) ?

Bem, Other é um tipo ? -able inteiro (como Result ), então eu não gostaria que throw Ok(4) funcionasse. (E deve ser toda a disjunção para evitar forçar a introdução de um tipo de erro artificial.) Por exemplo, acho que nossa interoperabilidade Opção-Resultado atual seria equivalente a isso (e o inverso):

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

Minha inclinação atual para throw seria para isto:

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

com

throw $expr  ==>  break 'closest_catch TryBreak::from_break($expr)
  • Estende TryContinue para que ok-wrapping também esteja disponível para o tipo de retorno do método no qual é usado
  • Não é um tipo associado, então você pode implantar, digamos, TryBreak<TryFromIntError> e TryBreak<io::Error> para um tipo, se desejar.

    • e impl<T> TryBreak<()> for Option<T> para habilitar apenas throw; parece plausível de uma forma que um tipo de erro associado de () não parecia.

    • impl<T> TryBreak<!> for T também seria bom, mas provavelmente é incoerente.

(À parte: esses nomes de características e métodos são terríveis; por favor, ajude !)

Meus pensamentos sobre as outras questões levantadas aqui não se consolidaram em uma forma facilmente articulável ainda, mas com relação aos problemas de inferência de tipo em torno do From::from() desugaring (não me lembro se isso também foi discutido em outro lugar recentemente , ou apenas aqui?):

A sensação instintiva (da experiência de Haskell) de que "dessa forma as ambigüidades de inferência do tipo mentem" foi uma das (embora não as principais) razões pelas quais eu não queria ter uma conversão de From como parte da RFC original. Agora que já está cozido no bolo, eu me pergunto se não poderíamos tentar ter esse bolo e comê-lo "apenas" adicionando uma regra especial de default para ele no processo de inferência de tipo.

Ou seja, eu penso: sempre que vemos um From::from() [opcionalmente: um que foi inserido pelo ? desugaring, ao invés de escrito manualmente], e sabemos exatamente um dos dois tipos (entrada vs. saída), enquanto o outro é ambíguo, assumimos o padrão do outro como sendo o mesmo. Em outras palavras, eu acho que, quando não está claro qual impl From usar, o padrão é impl<T> From<T> for T . Isso é, eu acho, basicamente sempre o que você realmente quer? Pode ser um pouco ad-hoc, mas se funcionar, os benefícios parecem valer a pena os custos IMHO.

(Eu também pensei que From já era um item lang, precisamente devido ao ? desugaring, mas não parece? Em qualquer caso, já é especial de alguma forma por esse motivo .)

na proposta de @scottmcm , From::from() _não_ faz parte da desauguração, mas sim na implementação de Try para Result .

@tmccombs Eu não estava propondo uma emenda à proposta dele.

O try{} RFC (https://github.com/rust-lang/rfcs/pull/2388) tem discutido try blocos onde não se preocupa com o resultado. Esta alternativa parece lidar com isso decentemente bem, já que pode escolher ignorar os tipos de erro inteiramente se assim o desejar, permitindo

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

Implementação de prova de conceito usando as mesmas características de antes: https://play.rust-lang.org/?gist=e0f6677632e0a9941ed1a67ca9ae9c98&version=stable

Acho que há algumas possibilidades interessantes aí, especialmente porque uma implementação customizada poderia, digamos, apenas pegar os resultados e vincular E: Debug forma que registre automaticamente qualquer erro que aconteça. Ou uma versão poderia ser feita especificamente como um tipo de retorno para main em conjunto com Termination que "simplesmente funciona" para permitir que você use ? sem uma assinatura de tipo complexo (https : //github.com/rust-lang/rfcs/issues/2367).

Tive problemas semelhantes aos evidenciados por @nikomatsakis usando a versão existente do traço Try . Para problemas específicos, consulte https://github.com/SergioBenitez/Rocket/issues/597#issuecomment -381533108.

As definições de características propostas por @scottmcm resolvem esses problemas. Eles parecem ser mais complicados do que o necessário, no entanto. Eu tentei reimplementá-los e descobri o seguinte:

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

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

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

A principal mudança é que o tipo Continue associado está no traço Try ao contrário do FromTry (anteriormente TryContinue ). Além de simplificar as definições, isso tem a vantagem de que Try pode ser implementado independentemente de FromTry , que considero ser mais comum, e que a implementação de FromTry é simplificada uma vez Try é implementado. (Observação: se desejarmos que Try e FromTry sejam implementados em uníssono, podemos simplesmente mover o método from_try para Try )

Veja o playground completo com implementações para Result e Option , bem como o Rocket Outcome neste playground .

Obrigado pelo relatório, @SergioBenitez! Essa implementação corresponde à versão alternativa "inverter os parâmetros de tipo" da proposta de característica original na RFC 1859: https://github.com/rust-lang/rfcs/blob/f89568b1fe5db4d01c4668e0d334d4a5abb023d8/text/0000-try-trait.md#unresolved -perguntas

A maior coisa que se perde é a propriedade de que typeof(x?) depende apenas de typeof(x) . A falta dessa propriedade era uma das preocupações com o original ("Eu me preocupo um pouco com a legibilidade do código ao longo dessas linhas" https://github.com/rust-lang/rfcs/pull/1859#issuecomment-279187967) e uma vantagem da proposta reducionista final ("Para qualquer tipo T dado,? pode produzir exatamente um tipo de valor ok / erro" https://github.com/rust-lang/rfcs/pull/1859#issuecomment-283104310). Claro, também houve argumentos de que essa propriedade é desnecessária ou muito restritiva, mas no resumo final antes do FCP ela ainda estava lá como uma vantagem (https://github.com/rust-lang/rfcs/pull/1859#issuecomment -295878466).

Além de simplificar as definições, isso tem a vantagem de que Try pode ser implementado independentemente de FromTry , o que eu considero mais comum

Certamente hoje from_ok é menos comum, pois Try e do catch são instáveis. E mesmo se do catch fosse estável, concordo que ele será usado menos de ? geral (já que a maioria desses blocos contém vários ? s).

Da perspectiva do traço e de suas operações, entretanto, acho que "envolver um valor 'continuar' no tipo de portador" é uma parte absolutamente essencial de ? . Raramente se passa pelo traço para isso hoje - apenas usando Ok(...) ou Some(...) vez disso - mas é crítico para uso genérico. Por exemplo, try_fold :

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

Se uma função vai retornar um tipo de portadora, é fundamental que haja uma maneira de colocar o valor de interesse nesse tipo de portadora. E, o mais importante, não acho que fornecer isso seja uma dificuldade. Tem uma definição muito natural para Resultado, Opção, Resultado, etc.

@Centril também apontou antes que "envolver um escalar em uma portadora" também é uma construção mais simples teoricamente. Por exemplo, Haskell a chama de Pointed typeclass , embora eu não ache que queremos generalizá-la _that_ tão em Rust: permitir try { 4 }vec![4] parece um exagero para mim .

Também estou imaginando um futuro onde, como async funções são propostas para envolver o valor do bloco em um futuro, podemos ter try funções que envolvem o valor do bloco em um tipo falível. Aqui, novamente, TryContinue é a parte crítica - tal função pode nem mesmo usar ? , se obtivermos uma construção throw .

Então, tudo isso é, infelizmente, mais filosófico do que concreto. Deixe-me saber se fez algum sentido, ou se você pensa o contrário em alguma das partes: ligeiramente_smiling_face:

Edit : Peço desculpas se você recebeu um e-mail com uma versão inicial deste; Eu apertei "comentar" muito cedo ...

A maior coisa que perde é a propriedade de que typeof (x?) Depende apenas de typeof (x).

Ah sim, absolutamente. Obrigado por apontar isso.

Claro, também houve argumentos de que essa propriedade é desnecessária ou muito restritiva, mas no resumo final antes do FCP ela ainda estava lá como uma vantagem (rust-lang / rfcs # 1859 (comentário)).

Existem exemplos específicos de onde isso pode ser muito restritivo?

Se uma função vai retornar um tipo de portadora, é fundamental que haja uma maneira de colocar o valor de interesse nesse tipo de portadora. E, o mais importante, não acho que fornecer isso seja uma dificuldade.

Acho que é uma análise justa. Eu concordo.

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

Portanto, a questão é: as restrições propostas são suficientes? Existem bons usos do contexto de caso de erro para determinar o tipo de sucesso? Existem abusos prováveis?

Posso dizer, por experiência própria com futuros, que pode muito bem haver alguns casos úteis aqui. Em particular, o tipo de pesquisa de que você fala pode ser processado de duas maneiras. Às vezes, queremos pular para a variante NotReady e ficar essencialmente com um valor Result. Às vezes, estamos interessados ​​apenas na variante Ready e queremos saltar para qualquer uma das outras variantes (como em seu esboço). Se permitirmos que o tipo de sucesso seja determinado em parte pelo tipo de erro, é mais plausível oferecer suporte a ambos os casos e, basicamente, usar o contexto para determinar que tipo de decomposição é desejado.

OTOH, eu me preocupo um pouco com a legibilidade do código ao longo dessas linhas. Isso parece qualitativamente diferente do que simplesmente converter o componente de erro - significa que o básico corresponde a isso? estaria executando depende de informações contextuais.

Então, pode-se imaginar um traço que move _both_ os tipos para digitar parâmetros, como

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

E use isso para habilitar todos

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

Mas acho que é definitivamente uma má ideia, pois significa que estes não funcionam

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

Uma vez que não há contexto de tipo para dizer o que produzir.

A outra versão seria permitir coisas como esta:

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

Meu instinto é que a inferência "fluindo do?" há muito surpreendente e, portanto, isso está no balde "muito inteligente".

De maneira crítica, não acho que não ter isso seja muito restritivo. my_result? em uma função -> Poll não precisa disso, pois o tipo de sucesso é o mesmo de costume (importante para manter o código síncrono funcionando da mesma forma em contextos assíncronos) e a variante de erro também converte bem . Usar ? em um Poll em um método que retorna Result parece um antipadrão de qualquer maneira, não algo que deveria ser comum, então usar métodos dedicados (hipotéticos) como .wait(): T (para bloquear para o resultado) ou .ready(): Option<T> (para verificar se está feito) para escolher o modo é provavelmente melhor de qualquer maneira.

Isso é "interessante" https://play.rust-lang.org/?gist=d3f2cd403981a631f30eba2c3166c1f4&version=nightly&mode=debug

Eu não gosto desses blocos try (do catch), eles não parecem muito amigáveis ​​para iniciantes.

Estou tentando montar o estado atual da proposta, que parece espalhado por vários comentários. Existe um único resumo do conjunto de características proposto atualmente (que elimina o tipo associado Error )?

No início deste tópico, vejo um comentário sobre a divisão de Try em características de erro separadas de / para - há algum plano para implementar essa divisão?

Seria útil ter uma conversão transparente de Result<T, E> para qualquer tipo Other<T2, E> no ponto de interrogação - isso permitiria que as funções IO existentes fossem chamadas com uma sintaxe agradável de dentro de uma função com uma mais especializada (por exemplo preguiçoso) tipo de retorno.

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

Semanticamente, parece From::from(E) -> Other<T2, E> , mas o uso de From está atualmente restrito à implementação Result -equivalente Try .

Eu realmente acho que NoneError deveria ter um problema de rastreamento separado. Mesmo que o traço Try nunca se estabilize, NoneError deve se estabilizar porque torna o uso de ? em Option muito mais ergonômico. Considere isso para erros como struct MyCustomSemanthicalError; ou erros de implementação de Default . None pode ser facilmente convertido em MyCustomSeemanthicalError por meio de From<NoneError> .

Ao trabalhar em https://github.com/rust-analyzer/rust-analyzer/ , encontrei um corte de papel ligeiramente diferente das insuficiências no operador ? , especialmente quando o tipo de retorno é Result<Option<T>, E> .

Para este tipo, faz sentido ? desugar de forma eficaz para:

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

onde value é do tipo Result<Option<V>, E> , ou:

value?

onde value é Result<V, E> . Acredito que isso seja possível se as bibliotecas padrão implementarem Try da seguinte maneira por Option<T> , embora eu não tenha testado isso explicitamente e acho que pode haver problemas de especialização.

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

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

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

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

Ao perguntar a @matklad por que ele não conseguiu criar um enum personalizado implementando Try , que seria chamado de Cancellable neste caso, ele apontou que std::ops::Try é instável, então não pode ser usado de qualquer maneira, visto que rust-analyzer (atualmente) tem como alvo a ferrugem estável.

Repostagem de https://github.com/rust-lang/rust/issues/31436#issuecomment -441408288 porque eu queria comentar sobre isso, mas acho que era o problema errado:

Essencialmente, uma situação que tenho são callbacks em um framework GUI - em vez de retornar Option ou Result , eles precisam retornar UpdateScreen , para informar ao framework se a tela precisa ser atualizado ou não. Freqüentemente, não preciso de nenhum registro (simplesmente não é prático fazer logon em todos os pequenos erros) e simplesmente retornar UpdateScreen::DontRedraw quando um erro ocorre. No entanto, com o atual operador ? , tenho que escrever isso o tempo todo:

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

Como não consigo converter de Result::Err em UpdateScreen::DontRedraw por meio do operador Try, isso se torna muito tedioso - muitas vezes eu tenho pesquisas simples em mapas de hash que podem falhar (o que não é um erro ) - com tanta frequência em um retorno de chamada, tenho de 5 a 10 usos do operador ? . Como o texto acima é muito prolixo de escrever, minha solução atual é impl From<Result<T>> for UpdateScreen assim e, em seguida, usar uma função interna no retorno de chamada como este:

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

    callback_inner(data).into()
}

Como o retorno de chamada é usado como um ponteiro de função, não posso usar um -> impl Into<UpdateScreen> (por algum motivo, retornar impl atualmente não é permitido para ponteiros de função). Portanto, a única maneira de usar o operador Try é fazer o truque da função interna. Seria bom se eu pudesse simplesmente fazer algo assim:

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

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

Não tenho certeza se isso seria possível com a proposta atual e apenas gostaria de adicionar meu caso de uso para consideração. Seria ótimo se um operador Try personalizado pudesse oferecer suporte a algo assim.

@joshtriplett Desculpe por demorar um pouco para voltar a isso. Eu montei um protótipo funcional da proposta em https://github.com/rust-lang/rust/compare/master...scottmcm : try-trait-v2 para ser concreto. Espero experimentar mais algumas coisas com ele.

@scottmcm Você tem alguma explicação de alto nível sobre as mudanças?

@scottmcm FWIW Eu tentei suas mudanças em rayon também:
https://github.com/rayon-rs/rayon/compare/master...cuviper : try-trait-v2
(ainda usando cópias privadas em vez de itens instáveis)

Então, qual é a solução para converter a Option NoneError? Parece que, por implementar o traço de teste, ele não compilará a menos que você habilite o uso desse recurso específico (instável?).

Então, basicamente, você não pode usar o? operador com opção, tanto quanto eu sei?

@omarabid , o operador é estável para uso com Option ou Result , mas você não pode usar Try como uma restrição genérica até que esteja estável. É perfeitamente normal usar ? em um Option em uma função que retorna Option , já que você não precisa envolver NoneError alguma. Você também pode devolver Result se apagar tipos:

use std::fmt::Debug;

pub struct Error(Box<dyn Debug>);

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

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

( playground )

@scottmcm , seu protótipo try-trait-v2 falha neste exemplo!

Se não quisermos que meu exemplo falhe, try-trait-v2 precisará de algo como:

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

Qual é o status atual deste recurso?

PR # 62606 para documentar a implementação de try_fold para iteradores deve ser reaberto assim que se tornar estável.

Editar: atualizou a operação com um item de rastreamento para esse ~ scottmcm

Há algum plano para substituir o traço Try por alguma das alternativas sugeridas neste tópico? A versão sugerida por @scottmcm parece boa. Quero continuar a usar o operador ? com Option e acho que o traço Try deve ser alterado para não forçar Result semântica em Option .

Usar a alternativa de @scottmcm nos permitiria usar ? com Option e nos livrar de NoneError . Concordo com @nikomatsakis ( comentário ) que o traço Try não deve promover a necessidade de "definir artificialmente um tipo para representar a 'falha' de um Option ".

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

Iniciante aqui, fui um pouco teimoso em querer pegar? para digitar automaticamente apagar qualquer erro e opção.
Depois de gastar muito tempo tentando entender por que outras soluções prováveis ​​não podem ser implementadas, descobri que @cuviper é o mais próximo do que posso obter.
Algumas explicações teriam ajudado, mas pelo menos consegui me familiarizar de perto com algumas limitações da metaprogramação de Rust.
Então, tentei descobrir e explicar em termos concretos.
Este tópico parece a encruzilhada mais provável onde eu espero poder ajudar alguém como eu tropeçando nisso, sinta-se à vontade para corrigir:

  • Usando Debug (comum a StdError e NoneError) em vez de StdError:
    Um From<T: StdError> for Error genérico e um conflito From<NoneError> for Error
    Porque seria ambíguo se NoneError implementasse StdError (aparentemente, mesmo o padrão não permite substituições e impõe exclusividade)
    Isso poderia ser resolvido por um "limite negativo" que não é suportado, talvez porque seria o único caso de uso? (sobre especialização)
    impl StdError para NoneError só pode ser definido ao lado de StdError ou NoneError em si, de modo que seja consistente em qualquer downstream.
  • O erro não pode ser um alias:
    type Error = Box<Debug> vincula a depuração que faz From<T:Debug> for Error entrar em conflito com From<T> for T (De reflexivo para idempotência)

Então, como Error não pode implementar Debug, você pode querer ter um auxiliar para desdobrar em um Resultado com Debug transitivo:

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

Não pode ser impl From<Result<T>> for StdResult<T> nem Into<StdResult> for Result<T> (não pode implantar o traço upstream para o tipo upstream)

Por exemplo, você pode usá-lo para obter um retorno de depuração para rescisão:

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

Idéia má: talvez o operador ? pudesse de alguma forma representar um vínculo monádico, então

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

torna-se

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

Uma ideia terrível, mas agrada o geek que existe em mim.

@derekdreery Não acho que funcionaria bem com fluxo de controle imperativo como return e continue

Lembre-se de que a semântica do operador ? já está estável. Apenas o traço Try real é instável e quaisquer alterações devem preservar o mesmo efeito estável para Option e Result .

@KrishnaSannasi

@derekdreery Não acho que funcionaria bem com fluxo de controle imperativo como retornar e continuar

Eu concordo com essa afirmação.

@cuviper

Lembre-se de que a semântica do? operador já estão estáveis. Apenas a característica Try real é instável, e quaisquer alterações devem preservar o mesmo efeito estável para Opção e Resultado.

Isso também se aplica a todas as épocas?

Em uma nota mais geral, não acho que seja possível unificar conceitos como .await , ?/ early return, Option :: map, Result :: map, Iterator :: map in rust, mas entender que todos eles compartilham alguma estrutura definitivamente me ajuda a ser um programador melhor.

Desculpas por ser OT.

Não tenho certeza se uma época / edição teria permissão para alterar ? desugaring, mas isso certamente complicaria muitas preocupações cruzadas, como macros.

Minha interpretação das garantias de estabilidade e RFC de épocas é que o comportamento de ? (açúcar ou outro) poderia ser alterado, mas seu comportamento atual nos tipos Option / Result deve permanecer o mesmo porque quebrar isso criaria muito mais rotatividade do que poderíamos esperar justificar.

E se Option alguma forma fosse um apelido de tipo para Result ? Ou seja, type Option<T> = Result<T, NoValue> , Option::<T>::Some(x) = Result::<T, NoValue>::Ok(x) , Option::<T>::None = Result::<T, NoValue>::Err(NoValue) ? Isso exigiria um pouco de mágica para implementar e não realista no ambiente de linguagem atual, mas talvez valha a pena explorar.

Não podemos fazer essa alteração porque existem impls de traços que dependem de Option an Result serem tipos distintos.

Se ignorarmos isso, poderíamos dar um passo adiante e até mesmo fazer de bool um alias para Result<True, False> , onde True e False são tipos de unidades.

Foi considerado adicionar um Try impl para a unidade () ? Para funções que não retornam nada, um retorno antecipado ainda pode ser útil no caso de um erro em uma função não crítica, como um retorno de chamada de registro. Ou a unidade foi excluída porque é preferível nunca ignorar silenciosamente os erros em qualquer situação?

Ou a unidade foi excluída porque é preferível nunca ignorar silenciosamente os erros em qualquer situação?

Esse. Se você deseja ignorar erros em um contexto não crítico, você deve usar unwrap ou uma de suas variantes.

ser capaz de usar ? em algo como foo() -> () seria bastante útil em um contexto de compilação condicional e deve ser fortemente considerado. Eu acho que isso é diferente da sua pergunta.

você deve usar desembrulhar ou uma de suas variantes.

unwrap() causa pânico, então eu não recomendo usá-lo em contextos não críticos. Não seria apropriado em situações em que um simples retorno é desejado. Alguém poderia argumentar que ? não é totalmente "silencioso" por causa do uso explícito e visível de ? no código-fonte. Desta forma, ? e unwrap() são análogos para funções de unidade, apenas com semânticas de retorno diferentes. A única diferença que vejo é que unwrap() causará efeitos colaterais visíveis (abortar o programa / imprimir um rastreamento de pilha) e ? não.

No momento, a ergonomia do retorno precoce em funções de unidade é consideravelmente pior do que naqueles retornando Result ou Option . Talvez esse estado de coisas seja desejável porque os usuários devem usar um tipo de retorno de Result ou Option nesses casos, e isso fornece um incentivo para eles mudarem sua API. De qualquer forma, pode ser bom incluir uma discussão sobre o tipo de devolução de unidade no PR ou na documentação.

ser capaz de usar ? em algo como foo() -> () seria bastante útil em um contexto de compilação condicional e deve ser fortemente considerado. Eu acho que isso é diferente da sua pergunta.

Como isso funcionaria? Apenas avalie sempre como Ok (()) e seja ignorado?

Além disso, você pode dar um exemplo de onde você deseja usar isso?

Sim, a ideia era que algo como MyCollection :: push pode, dependendo da configuração do tempo de compilação, ter um valor de retorno Result <(), AllocError> ou um valor de retorno () se a coleção estiver configurada para apenas entrar em pânico em caso de erro. O código intermediário que usa a coleção não deveria ter que pensar sobre isso, então se ele pudesse simplesmente _sempre_ usar ? mesmo quando o tipo de retorno for () , seria útil.

Depois de quase 3 anos, isso está mais perto de ser resolvido?

@Lokathor que só funcionaria se um tipo de retorno Result<Result<X,Y>,Z> ou similar não fosse possível. Mas isso é. Portanto, não é possível.

Não entendo por que um Resultado em camadas causa problemas. Você poderia explicar melhor?

Para fins de referência cruzada, uma formulação alternativa foi proposta em internos por @dureuill :

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

@Lokathor
Ok, pensei sobre isso mais profundamente e acho que posso ter encontrado uma explicação bastante boa.

Usando uma anotação

O problema é a interpretação do tipo de retorno, ou anotações estranhas confundiriam o código. Seria possível, mas tornaria o código mais difícil de ler. (Pré-condição: #[debug_result] aplica seu comportamento desejado e modifica uma função para entrar em pânico no modo de liberação em vez de retornar Result::Err(...) e desembrulhar Result::Ok , mas essa parte é complicada)

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

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

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

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

    Ok(z)
}

Assim, tornaria o código mais difícil de ler.
Não podemos simplesmente modificar o comportamento de ? simplesmente para um ambiente autônomo,
especialmente se o valor de retorno de um #[debug_result] for salvo em uma variável temporária e posteriormente "propagado por tentativa" com o operador ? . Isso tornaria a semântica de ? realmente confusa, porque dependeria de muitas "variáveis", que não são necessariamente conhecidas na "hora da escrita" ou podem ser difíceis de adivinhar apenas lendo o código-fonte. #[debug_result] precisaria estragar as variáveis ​​às quais foram atribuídos valores de função, mas não funcionará se uma função for marcada com #[debug_result] e outra não, por exemplo, o seguinte seria um erro de tipo.

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

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

Alternativa: usar um novo tipo

Uma solução "mais limpa" pode ser um tipo DebugResult<T, E> que só entra em pânico quando um certo sinalizador de compilação é definido e é construído a partir de um erro, mas de outra forma seria equivalente a Result<T, E> . Mas isso também seria possível com a proposta atual, eu acho.

Resposta ao último post de @zserik : A macro que você descreveu é inútil - o tipo de retorno de cobrança da função com base na configuração de compilação interromperá todas as correspondências no resultado da função em todas as configurações de compilação possíveis, exceto a única, independentemente da maneira como foi feito.

@ tema3210 eu sei. Eu só queria apontar que a ideia de @Lokathor geralmente não funcionaria na prática. A única coisa que pode funcionar parcialmente é o seguinte, mas apenas com muitas restrições, que não acho que valham a pena a longo prazo:

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

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

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

//..end eval

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

@zserik É possível que realmente tenha uma forma como esta?

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

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

Ok, boa ideia.

Não tenho certeza se isso é algo que precisa ser considerado antes da estabilização, mas estou interessado em implementar rastreamentos de retorno de erro e acho que envolve mudanças no traço Try ou pelo menos em seu impl fornecido por Result para fazê-lo funcionar. Eventualmente, pretendo transformar isso em um RFC, mas quero ter certeza de que o traço try não está estabilizado de uma forma que torne impossível adicioná-lo posteriormente, caso demore um pouco para escrever esse RFC. A ideia básica é esta.

Você tem uma característica que usa para passar informações de rastreamento que usa especialização e um impl padrão para T para manter a compatibilidade com versões anteriores

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

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

E então você modifica a implementação de Try para Result para usar track_caller e passa esta informação para o tipo interno,

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

E então, para os tipos que você deseja reunir rastros anteriores para implementar o Track

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

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

O uso acaba ficando assim

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

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

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

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

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

E a saída de uma versão muito ruim de um backtrace se parece com isso

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

E aqui está uma prova de conceito em ação https://github.com/yaahc/error-return-traces

Achei que apenas um tipo de erro que podemos converter para tentar o implementador de características pode ser insuficiente, então, poderíamos fornecer uma interface como esta:

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

Observe que o compilador pode monomorfizar from_error , evitando a chamada de From::from , e pode-se fornecer o método impl para diferentes tipos de erro manualmente, resultando na capacidade do operador ? de "desembrulhar" diferentes tipos de erros.

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

Conforme escrito, o implementador teria que aceitar _qualquer_ tamanho T , sem restrições. Se você quisesse permitir uma restrição personalizada, teria que ser um parâmetro de característica, como Try<T> . Isso é semelhante à combinação TryBlock / Bubble<Other> que @scottmcm tinha em https://github.com/rust-lang/rust/issues/42327#issuecomment-457353299 .

Conforme escrito, o implementador teria que aceitar _qualquer_ tamanho T , sem restrições. Se você quisesse permitir uma restrição personalizada, teria que ser um parâmetro de característica, como Try<T> . Isso é semelhante à combinação TryBlock / Bubble<Other> que @scottmcm tinha em # 42327 (comentário) .

Eu quis dizer que o uso deve ser assim:

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

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

você precisa dividir o Try e o TryFromError. Gosto mais disso do que da proposta original fwiw, mas acho que seria necessário um novo RFC.

(e ainda acho que deveria ter sido chamado de "propagar" em vez de "tentar", mas estou divagando)

@ tema3210 Acho que entendi sua intenção, mas isso não é válido, Rust.

@ SoniEx2

você precisa dividir o Try e o TryFromError.

Certo, é por isso que mencionei o design de TryBlock / Bubble<Other> . Podemos debater essa escolha de nomenclatura, mas a ideia era que o retorno antecipado nem sempre é sobre _errores_, por si só. Por exemplo, muitos dos métodos Iterator internos estão usando um tipo LoopState . Para algo como find , não é um erro encontrar o que você está procurando, mas queremos parar a iteração aí.

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

Podemos debater essa escolha de nomenclatura, mas a ideia era que o retorno antecipado nem sempre envolve erros, per se.

precisamente porque não gosto do nome "try" e prefiro o nome "propagar", porque "propaga" um retorno "precoce": p

(não tenho certeza se isso faz sentido? da última vez que mencionei isso, a coisa de "propagar" só fez sentido para mim por algum motivo e eu nunca fui capaz de explicar para os outros).

Essa característica será de alguma ajuda ao tentar sobrescrever o comportamento padrão ? fe para adicionar um gancho de log fe para registrar informações de depuração (como número de arquivo / linha)?

Atualmente há suporte para sobrescrever macros stdlib, mas parece que o operador ? não é convertido para a macro try! explicitamente. Isso é lamentável.

@stevenroose Para adicionar suporte para isso apenas ao traço Try , seria necessária uma modificação do traço Try para incluir informações de localização do arquivo sobre o local onde ? "aconteceu" .

@stevenroose Para adicionar suporte para isso apenas ao traço Try , seria necessária uma modificação do traço Try para incluir informações de localização do arquivo sobre o local onde ? "aconteceu" .

Isso não é verdade, # [track_caller] pode ser usado em impls de traits mesmo se a definição de trait não incluir a anotação

@stevenroose para responder à sua pergunta, sim, embora se você estiver interessado em imprimir todos os ? locais que um erro propaga através, você deve verificar o comentário de rastreamento de retorno de erro acima

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

Não tenho certeza se alguém já mencionou isso, vamos impl Try for bool , talvez Try<Ok=(), Error=FalseError> ?
Para que pudéssemos fazer algo assim

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

Agora eu tenho que usar o tipo de retorno Option<()> na maioria dos casos onde acho que ? poderia tornar o código muito mais claro.

Não tenho certeza se alguém já mencionou isso, vamos impl Try for bool , talvez Try<Ok=(), Error=FalseError> ?

Isso faria com que Try se comportasse como o operador && em bool s. Como outros apontaram acima, também existem casos de uso para curto-circuito em caso de sucesso, caso em que Try deve se comportar como || . Como os operadores && e || não demoram muito para digitar, também não vejo muita vantagem em ter essa implementação.

@calebsander obrigado pela gentil resposta.
Isso é verdade para alguns casos simples, mas não acho que seja frequentemente o caso, especialmente nunca poderíamos ter instruções de atribuição como let x = v.get_mut(key)? na expressão.
Se && e || sempre funcionassem, poderíamos também jogar com .unwrap_or_else() , .and_then() para Option e Error casos.
Você poderia expressar o código corrente em && e || ?

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

Para alguma condição em que true significa falha enquanto false significa sucesso, !expr? pode ser confuso, mas poderíamos usar expr.not()? para fazer o truque (nota: para ops::Try , sempre temos false para Error )

Isso é verdade para alguns casos simples, mas não acho que seja frequentemente o caso, especialmente nunca poderíamos ter instruções de atribuição como let x = v.get_mut(key)? na expressão.

Bem, a menos que eu não tenha entendido sua proposta, apenas implementar Try<Ok = (), Error = FalseError> para bool não permitiria isso. Você também precisaria impl From<NoneError> for FalseError para que o operador ? pudesse converter None em false . (E da mesma forma, se você quiser aplicar ? a um Result<T, E> dentro de uma função que retorna bool , você precisaria de impl From<E> for FalseError . Acho que tal a implementação geral seria problemática.) Você também pode usar some_option().ok_or(false)? e some_result().map_err(|_| false)? se concordar com o valor de retorno Result<bool, bool> (que pode ser recolhido com .unwrap_or_else(|err| err) ).

Deixando de lado as questões de conversão de outros Try erros em bool , a implementação Try você está propondo para bool é apenas o operador && . Por exemplo, eles são equivalentes

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

e

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

(Reconhecidamente, os fluxos de controle como if e loop que não retornam () são menos fáceis de traduzir.)

Para alguma condição em que true significa falha enquanto false significa sucesso, !expr? pode ser confuso, mas poderíamos usar expr.not()? para fazer o truque (nota: para ops::Try , sempre temos false para Error )

Não está claro para mim que false deva representar o caso Error . (Por exemplo, Iterator.all() iria querer false entrar em curto-circuito, mas Iterator::any() iria querer true vez disso.) Como você apontou, você pode obter o comportamento oposto de curto-circuito, invertendo o valor passado para ? (e invertendo também o valor de retorno da função). Mas não acho que isso resulte em um código muito legível. Pode fazer mais sentido separar struct s And(bool) e Or(bool) que implementam os dois comportamentos diferentes.

E da mesma forma, se você deseja se inscrever? para um resultadodentro de uma função que retorna bool, você precisaria implantar Frompara FalseError. Acho que essa implementação abrangente seria problemática.

Não, eu não quero impl From<T> for FalseError , talvez pudéssemos fazer result.ok()?

Não está claro para mim que false deva representar o caso de Erro.

Acho que é de alguma forma natural quando temos bool::then que mapeiam true para Some .

Perdoe-me se perdi - por que NoneError impl std::error::Error ? Isso o torna inútil para qualquer função que retorne Box<dyn Error> ou similar.

Perdoe-me se perdi - por que NoneError impl std::error::Error ? Isso o torna inútil para qualquer função que retorne Box<dyn Error> ou similar.

Não sou um especialista aqui, mas posso ver problemas significativos ao tentar chegar a uma mensagem de erro útil para "algo era None e você esperava que fosse Some " (que é basicamente o que você estaria ganhando aqui - alguma mensagem de diagnóstico). Na minha experiência, sempre fez mais sentido usar o combinador Option::ok_or_else para usar outro tipo de erro, porque como código de chamada geralmente tenho um contexto muito melhor para fornecer de qualquer maneira.

Eu concordo com @ErichDonGubler. É muito chato não poder usar ? com Option , no entanto, é por um bom motivo e, como um usuário de linguagem, acho que é do interesse de todos lidar com os erros de maneira adequada. Fazer esse trabalho extra de vincular o contexto de erro a None vez de apenas fazer ? é muito, muito chato, mas força a comunicação correta dos erros, o que vale a pena.

A única vez em que eu permitiria que None fosse interpretado como um erro é em uma prototipagem muito grosseira. Achei que, se adicionarmos algo como uma chamada Option::todo_err() , isso retornaria Ok do valor interno, mas entraria em pânico em None . É muito contra-intuitivo até que você analise as necessidades do modo de "prototipagem bruta" de autoria de código. É muito semelhante a Ok(my_option.unwrap()) , mas não precisa de uma embalagem Ok(...) . Além disso, ele especifica explicitamente a natureza "todo", comunicando assim a intenção de remover esse código da lógica de produção, substituindo-o por uma associação de erro adequada.

mas entrará em pânico em nenhum

Sinto fortemente que devemos apenas deixar isso para unwrap . Se adicionarmos um todo_err, ele deve retornar um tipo de erro real, o que levanta a questão de qual tipo de erro retornar.

Além disso, suspeito que fn todo_err(self) -> Result<Self, !> teria o problema óbvio de exigir ! para estabilizar, o que, uh, bem, Algum dia.

Meu caso de uso é de fato uma prototipagem onde não me importo muito com os erros. Todo o NoneError sofre com os problemas que você listou? Se for decidido que ele deve existir (o que eu acho uma coisa boa, pelo menos para prototipagem), eu acredito que ele deve implantar Error já que foi nomeado um.

Por causa do tipo que não tinha este impl, recorri a apenas colocar .ok_or("error msg") nele, o que também funciona, mas é menos conveniente.

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