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)
Iterator::try_fold
fold
implementado em termos de try_fold
, para que ambos não precisem ser substituídos.)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.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
.
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.).
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.
Ao usar try_fold
para implementar position
(e um monte de outros métodos iteradores), fiquei tão confuso que era o mesmo quando usei Result
(meu cérebro não gostou de find
encontrando a coisa sendo Err
) que eu fiz meu próprio enum com Break
e Continue
variantes em vez disso.
Ao escrever tree_fold1
em itertools , acabei com um estranho match
que a função interna sempre retorna Err
. Há uma desconexão estranha de que None
é um "erro" de Iterator::next()
, mas ao mesmo tempo é "sucesso" de fold
, já que você precisa chegar ao fim a ser feito. E é muito conveniente usar ?
(bem, try!
naquele código, pois ele precisa ser compilado no Rust 1.12) para lidar com a propagação da condição final.
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ê:
É 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:
TryContinue
para seu tipo de argumento, de modo que o tipo de uma expressão x?
seja sempre o mesmoSelf
para casos simples como try_fold
que inspecionam e retornam o mesmo tipo, portanto, tudo bem com apenas T: Try
.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 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:
Result
impl que eu realmente não gostoLessOrGreater
extra no implemento Ordering
.From
consistenteStrandResult
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.throw
?
como Ok(x) => x, Err(r) => throw e.into()
que eu realmente gosteithrow;
ser a maneira de produzir None
(por meio de algo como impl<T> Throw<()> for Option<T>
), que é muito melhor do que throw NoneError;
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)
TryContinue
para que ok-wrapping também esteja disponível para o tipo de retorno do método no qual é usadoTryBreak<TryFromIntError>
e TryBreak<io::Error>
para um tipo, se desejar.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 deFromTry
, 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
:
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:
From<T: StdError> for Error
genérico e um conflito From<NoneError> for Error
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 comofoo() -> ()
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 :
@Lokathor
Ok, pensei sobre isso mais profundamente e acho que posso ter encontrado uma explicação bastante boa.
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()?
};
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, comoTry<T>
. Isso é semelhante à combinaçãoTryBlock
/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í.
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çoTry
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
, talvezTry<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 enquantofalse
significa sucesso,!expr?
pode ser confuso, mas poderíamos usarexpr.not()?
para fazer o truque (nota: paraops::Try
, sempre temosfalse
paraError
)
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 resultado
dentro de uma função que retorna bool, você precisaria implantar From para 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
implstd::error::Error
? Isso o torna inútil para qualquer função que retorneBox<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.
Comentários muito úteis
Qual é o status atual deste recurso?