Rust: Problema de seguimiento para `ops :: Try` (función` try_trait`)

Creado en 31 may. 2017  ·  99Comentarios  ·  Fuente: rust-lang/rust

El rasgo Try de https://github.com/rust-lang/rfcs/pull/1859; implementado en PR https://github.com/rust-lang/rust/pull/42275.

Separarse de https://github.com/rust-lang/rust/issues/31436 para mayor claridad (según https://github.com/rust-lang/rust/pull/42275#discussion_r119167966)

  • [] Estabilizar esto permitirá que las personas implementen Iterator::try_fold

    • [] Como parte de la estabilización, vuelva a abrir el PR # 62606 para documentar la implementación de try_fold para iteradores.

    • [] Asegúrese de que las implementaciones predeterminadas de otras cosas tengan el DAG deseado a largo plazo, ya que cambiarlas es esencialmente imposible más adelante. (Específicamente, sería bueno tener fold implementado en términos de try_fold , para que no sea necesario anular ambos).

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

Comentario más útil

¿Cuál es el estado actual de esta función?

Todos 99 comentarios

Un par de piezas de desguace de bicicletas:

  • ¿Tenemos una motivación particular para llamar al tipo asociado Error lugar de Err ? Llamarlo Err lo alinearía con Result : el otro ya se llama Ok .

  • ¿Tenemos una motivación particular para tener métodos separados from_error y from_ok , en lugar de un solo from_result que sería más simétrico con into_result que es el otro la mitad del rasgo?

( versión actualizada del enlace del patio de recreo del RFC )

@glaebhoerl

  • Error vs Err se discutió en relación con TryFrom en https://github.com/rust-lang/rust/issues/33417#issuecomment -269108968 y https: // github.com/rust-lang/rust/pull/40281; Supongo que el nombre fue elegido aquí por razones similares.
  • Creo que están separados porque tienen usos diferentes, y espero que sea raro que alguien realmente tenga un Result que esté tratando de convertir en un T:Try . Prefiero Try::from_ok y Try::from_error a llamar siempre a Try::from_result(Ok( y Try::from_result(Err( , y estoy feliz de simplemente implicar los dos métodos en lugar de escribir la coincidencia. Quizás sea porque pienso en into_result no como Into<Result> , sino como "¿pasó o falló?", Siendo el tipo específico Result como un detalle de implementación sin importancia. (No quiero sugerir o reabrir "debería haber un nuevo tipo de valor de producción frente a devolución anticipada, sin embargo). Y para la documentación, me gusta que from_error hable de ? (o eventualmente throw ), mientras que from_ok habla sobre el ajuste correcto (# 41414), en lugar de tener ambos en el mismo método.

No estoy seguro de si este es el foro correcto para este comentario, por favor redirigirme si no es: smiley : https://github.com/rust-lang/rfcs/pull/1859 y me perdí el período de comentarios; ¡UPS!


Me preguntaba si hay un caso para dividir el rasgo Try o eliminar el método into_result ; para mí actualmente Try es algo así como la suma de rasgos FromResult (que contiene from_error y from_ok ) y IntoResult (que contiene into_result ).

FromResult permite una salida anticipada altamente ergonómica con el operador ? , que creo que es el mejor caso de uso para la función. Creo que IntoResult ya se puede implementar perfectamente con métodos por caso de uso o como Into<Result> ; ¿Me faltan algunos ejemplos útiles?

Siguiendo el RFC del rasgo Try , expr? podría desugar como:

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

Los ejemplos motivadores que consideré son Future y Option .

Futuro

Podemos implementar FromResult naturalmente por 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)
    }
}

Suponiendo una implementación razonable para ? que regresará de una función valorada impl Future cuando se aplique a una Result::Err entonces puedo escribir:

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

¡Eso es exactamente lo que estaba tratando de implementar hoy y Try clava! Pero si intentamos implementar el actual Try para Future quizás no haya una opción canónica para into_result ; podría ser útil entrar en pánico, bloquear o sondear una vez, pero ninguno de estos parece universalmente útil. Si no hubiera into_result en Try , puedo implementar una salida anticipada como se indicó anteriormente, y si necesito convertir un Future en un Result (y de ahí a any Try ) Puedo convertirlo con un método adecuado (llamar al método wait para bloquear, llamar a algunos poll_once() -> Result<T,E> , etc.).

Opción

Option es un caso similar. Implementamos from_ok , from_err naturalmente como en el RFC del rasgo Try , y podríamos convertir Option<T> en Result<T, Missing> simplemente con o.ok_or(Missing) o un método de conveniencia ok_or_missing .


¡Espero que ayude!

Quizás llego muy tarde a todo esto, pero este fin de semana se me ocurrió que ? tiene una semántica bastante natural en los casos en los que te gustaría hacer un cortocircuito en _success_.

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

Pero en este caso, el nombre de los métodos de rasgos Try no encaja.

Sin embargo, ampliaría el significado del operador ? .

Al crear un prototipo de try_fold para rayón , me encontré queriendo algo como Try::is_error(&self) para los métodos Consumer::full() . (El rayón tiene esto porque los consumidores son básicamente iteradores de estilo push, pero aún queremos cortocircuitar los errores). En su lugar, tuve que almacenar los valores intermedios como Result<T::Ok, T::Err> para poder llamar Result::is_err() .

A partir de ahí también deseaba un Try::from_result para volver a T al final, pero un mapeo de match a T::from_ok y T::from_error no es _demasiado_ malo.

El rasgo Try puede proporcionar un método from_result para la ergonomía si se requieren from_ok y from_error . O viceversa. De los ejemplos dados veo un caso para ofrecer ambos.

Dado que PR # 42275 se ha fusionado, ¿significa eso que este problema se ha resuelto?

@skade Tenga en cuenta que un tipo puede definir cualquiera que sea el que está en cortocircuito, como lo hace LoopState para algunos internos de Iterator . Quizás eso sería más natural si Try hablara de "continuar o romper" en lugar de éxito o fracaso.

@cuviper La versión que terminé necesitando era el tipo Ok o el tipo Error -in-original. Espero que desestructurar y reconstruir sea algo bastante general que se optimice bien y que no se necesiten un montón de métodos especiales sobre el rasgo.

@ErichDonGubler Este es un problema de seguimiento, por lo que no se resuelve hasta que el código correspondiente esté estable.

Informe de experiencia:

Me he sentido un poco frustrado al intentar poner en práctica este rasgo. Varias veces he tenido la tentación de definir mi propia variante en Result por la razón que sea, pero cada vez terminé usando Result al final, principalmente porque implementé Try era demasiado molesto. ¡Sin embargo, no estoy del todo seguro de si esto es algo bueno o no!

Ejemplo:

En el nuevo solucionador de Chalk VM, quería tener una enumeración que indicara el resultado de resolver una "hebra". Esto tenía cuatro posibilidades:

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

Quería ? , cuando se aplica a esta enumeración, para desenvolver "éxito" pero propagar todos los demás fallos hacia arriba. Sin embargo, para implementar el rasgo Try , habría tenido que definir una especie de tipo "residual" que encapsule solo los casos de error:

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

Pero una vez que tenga este tipo, entonces también podría hacer StrandResult<T> un alias:

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

y esto es lo que hice.

Ahora, esto no parece necesariamente peor que tener una sola enumeración, pero se siente un poco extraño. Por lo general, cuando escribo la documentación para una función, por ejemplo, no "agrupo" los resultados por "ok" y "error", sino que hablo de las diversas posibilidades como "iguales". Por ejemplo:

    /// 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.

Tenga en cuenta que no dije "devolvemos Err(StrandFail::NoSolution) . Esto se debe a que el Err siente como este artefacto molesto que tengo que agregar.

(Por otro lado, la definición actual ayudaría a los lectores a saber cuál es el comportamiento de ? sin consultar el Try impl.)

Supongo que este resultado no es tan sorprendente: la Try impl actual te obliga a usar ? en cosas que son isomorfas a Result . Esta uniformidad no es accidental, pero como resultado, hace que sea molesto usar ? con cosas que no son básicamente "solo Result ". (Para el caso, es básicamente el mismo problema que da lugar a NoneError : la necesidad de definir artificialmente un tipo para representar el "fallo" de un Option ).

También señalaría que, en el caso de StrandFail , no quiero particularmente la conversión From::from que obtienen los resultados normales, aunque no me perjudica.

¿El tipo Try::Error asociado alguna vez está expuesto / utilizado por el usuario directamente? ¿O es simplemente necesario como parte de la eliminación de azúcar de ? ? Si es lo último, no veo ningún problema real con solo definirlo "estructuralmente" - type Error = Option<Option<(Strand, Minimums)>> o lo que sea. (Tener que averiguar el equivalente estructural de la "mitad de falla" de la definición enum no es genial, pero parece menos molesto que tener que reajustar toda la API de cara al público).

Yo tampoco lo sigo. Implementé con éxito Try para un tipo que tenía una representación de error interno y me pareció natural que Ok y Error fueran del mismo tipo. Puede ver la implementación aquí: https://github.com/kevincox/ecl/blob/8ca7ad2bc4775c5cfc8eb9f4309b2666e5163e02/src/lib.rs#L298 -L308

De hecho, parece que hay un patrón bastante simple para hacer que esto funcione.

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

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

Si desea desenvolver el éxito, algo como esto debería 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),
        }
    }
}

Es posible definir un tipo estructural, pero resulta bastante molesto. Estoy de acuerdo en que podría usar Self . Sin embargo, me parece un poco extraño que luego puedas usar ? para convertir de StrandResult a Result<_, StrandResult> etc.

¡Gran informe de experiencia, @nikomatsakis! También estoy insatisfecho con Try (desde la otra dirección).

TL / DR : Creo que FoldWhile hizo esto bien, y deberíamos duplicar la interpretación Break -vs- Continue de ? lugar de hablar de en términos de errores.

Más extenso:

Sigo usando Err para algo más cercano al "éxito" que al "error" porque ? es muy conveniente.

  • Cuando usé try_fold para implementar position (y un montón de otros métodos iteradores), me confundí tanto que estaba tan arriba cuando usaba Result (a mi cerebro no le gustó find encontrando que era un Err ) que hice mi propia enumeración con las variantes Break y Continue .

  • Al escribir tree_fold1 en itertools , terminé con un match extraño None es un "error" de Iterator::next() , pero al mismo tiempo es un "éxito" de un fold , ya que necesitas llegar al final para acabar. Y es muy conveniente usar ? (bueno, try! en ese código ya que necesita compilarse en Rust 1.12) para lidiar con la propagación de la condición final.

Entonces, al menos, creo que la descripción que escribí para Try es incorrecta y no debería hablar de una "dicotomía éxito / fracaso", sino más bien abstraerse de los "errores".

La otra cosa en la que he estado pensando es que deberíamos considerar algunas implicaciones un poco locas por Try . Por ejemplo, Ordering: Try<Ok = (), Error = GreaterOrLess> , combinado con "funciones de prueba", podría permitir que cmp por struct Foo<T, U> { a: T, b: U } solo sean

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

Todavía no sé si ese tipo de locura es buena o mala: reír: ciertamente tiene algo de elegancia, ya que una vez que las cosas son diferentes, sabes que son diferentes. Y tratar de asignar "éxito" o "error" a cualquier lado de eso no parece encajar bien.

También noto que esa es otra instancia en la que la "conversión de error" no es útil. Tampoco lo usé nunca en los ejemplos anteriores "¿Quiero? Pero no es un error". Y ciertamente se sabe que causa tristeza por inferencia, así que me pregunto si es algo que debería limitarse solo al Resultado (o potencialmente eliminarse a favor de .map_err(Into::into) , pero eso probablemente no sea factible).

Editar: Ah, y todo eso me hace preguntarme si quizás la respuesta a "Sigo usando Result para mis errores en lugar de implementar Try para un tipo propio" es "buena, eso se espera".

Edición 2: esto no es diferente a https://github.com/rust-lang/rust/issues/42327#issuecomment -318923393 anterior

Edición 3: parece que también se propuso una variante Continue en https://github.com/rust-lang/rfcs/pull/1859#issuecomment -273985250

Mis dos centavos:

Me gusta la sugerencia de @fluffysquirrels de dividir el rasgo en dos rasgos. Uno para convertir a un resultado y otro para convertir a partir de un resultado. Pero sí creo que deberíamos mantener el into_result o equivalente como parte del desugaring. Y creo que en este punto tenemos que hacerlo, ya que el uso de Option como Try ha estabilizado.

También me gusta la idea de @scottmcm de usar nombres que sugieran romper / continuar en lugar de error / ok.

Para poner el código específico aquí, me gusta cómo se lee:

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

Por supuesto, no es tan agradable como un bucle directo, pero con "métodos de prueba" estaría cerca:

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

A modo de comparación, encuentro la versión de "vocabulario de errores" realmente engañosa:

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

¿Podemos implementar Display para NoneError? Permitiría que la caja de fallas derive automáticamente From<NoneError> for failure::Error . Ver https://github.com/rust-lang-nursery/failure/issues/61
Debería ser un cambio de 3 líneas, pero no estoy seguro del proceso de RFC y demás.

@ cowang4 Me gustaría intentar evitar permitir más mezcla de Result-and-Option en este momento, ya que el tipo es inestable principalmente para mantener nuestras opciones abiertas allí. No me sorprendería si termináramos cambiando el diseño de Try y el desugar en algo que no necesite NoneError ...

@scottmcm Está bien. Te entiendo. Eventualmente me gustaría una forma limpia de endulzar el patrón de devolver Err cuando una función de biblioteca devuelve None. ¿Quizás conoces otro que no sea Try ? Ejemplo:

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

Una vez que encontré esta función experimental y la caja de failure , naturalmente gravité hacia:

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

Lo cual _casi_ funciona, excepto por la falta de un impl Display for NoneError como mencioné antes.
Pero, si esta no es la sintaxis con la que nos gustaría ir, entonces tal vez podría haber otra función / macro que simplifique el patrón:

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

@ cowang4 Creo que eso funcionaría si implementaras From<NoneError> para failure::Error , que usó tu propio tipo que implementó Display .

Sin embargo, probablemente sea una mejor práctica usar opt.ok_or(_)? para que pueda decir explícitamente cuál debería ser el error si la opción es Ninguna. En su ejemplo, por ejemplo, es posible que desee un error diferente si pb.file_stem es Ninguno que si to_str() devuelve Ninguno.

@tmccombs Intenté crear mi propio tipo de error, pero debo haberlo hecho mal. Fue así:

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

Y luego intenté usar mi tipo de error ...

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`

De acuerdo, me acabo de dar cuenta de que quiere saber cómo convertir de otros tipos de error a mi error, que puedo escribir de forma genérica:

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

No...

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;

Bien, ¿qué pasa con std::error::Error ?

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

Eso tampoco funciona. En parte porque entra en conflicto con mi 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

Lo cual es extraño porque pensé que NoneError no implementó std::error::Error . Cuando comento mis impl From<NoneError> no genéricos obtengo:

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`

¿Tengo que escribir todos los From s manualmente? ¿Pensé que se suponía que la caja de fallas los derivaría?

Tal vez debería quedarme con option.ok_or()

¿Tengo que escribir todos los From manualmente? ¿Pensé que se suponía que la caja de fallas los derivaría?

No creo que la caja de fallos haga eso. Pero podría estar equivocado.

Ok, volví a examinar la caja de fallas, y si estoy leyendo bien la documentación y las diferentes versiones, está diseñado para usar siempre failure::Error como el tipo de error en su Result s, ver aquí . Además, implementa un rasgo general impl Fail para la mayoría de los tipos de error:

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

Y luego un impl From para que pueda Try / ? otros errores (como los de std) en el tipo general 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

Pero, es solo que dado que el error relevante para este problema de óxido, NoneError , es experimental, todavía no se puede convertir automáticamente, porque no implementa el rasgo Display . Y no queremos que lo haga, porque eso desdibuja la línea entre Option sy Result s más. Es probable que todo se vuelva a trabajar y se solucione eventualmente, pero por ahora, me ceñiré a las técnicas sin azúcar que he aprendido.

Gracias a todos por ayudar. ¡Estoy aprendiendo poco a poco a Rust! :sonrisa:

Me ceñiré a las técnicas sin azúcar que he aprendido.

: +1: Honestamente, creo que .ok_or(...)? seguirá siendo el camino a seguir (o .ok_or_else(|| ...)? , por supuesto). Incluso si NoneError _ tuviera_ una Display impl, ¿qué diría? "¿Algo no estaba allí"? Eso no es un gran error ...

Intentando acercarse a una propuesta concreta ...

Me empieza a gustar la alternativa TrySuccess . Curiosamente, creo que a nadie, incluido yo mismo, le gustó ese originalmente; ni siquiera está en la versión final del RFC. Pero afortunadamente sigue vivo en la historia: https://github.com/rust-lang/rfcs/blob/f89568b1fe5db4d01c4668e0d334d4a5abb023d8/text/0000-try-trait.md#using -an-associated-type-for-the-success -valor

Me parece que la mayor objeción fue la queja razonable de que un rasgo central completo solo para un tipo asociado ( trait TrySuccess { type Success; } ) era excesivo. Pero con catch try bloques atrás (https://github.com/rust-lang/rust/issues/41414#issuecomment-373985777) para hacer un ajuste correcto (como en el RFC) , de repente tiene un uso directo e importante: este es el rasgo que controla ese envoltorio. Junto con el objetivo de " ? siempre debe producir el mismo tipo" que creo que en general se deseaba, el rasgo parece sostener mejor su peso. Hombre de paja:

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

Ese tipo asociado, como el que será devuelto por el operador ? , es también el que claramente necesita existir. Eso significa que no golpea la molestia "tenía que definir una especie de tipo 'residual'" que articuló Niko . Y ser () es razonable, incluso común, por lo que evita contorsiones similares a NoneError .

Editar: Para el cobertizo de bicicletas, "retorno" podría ser una buena palabra, ya que esto es lo que sucede cuando return de un método try (también conocido como ok-wrap). return es también el nombre del operador de mónada para esto, iiuc ...

Edición 2: Mis pensamientos sobre los otros rasgos / métodos aún no se han estabilizado, @tmccombs.

@scottmcm solo para ser claros, ¿o sugieres los siguientes dos rasgos?

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

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

Y desuguring x? se vería así:

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
}

y try { ...; expr} desugaría a algo como:

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

@scottmcm Yo también encuentro esa variante mucho más atractiva cuando considero ok-wrapping =)

Continuando con el siguiente rasgo, creo que el de ? convierte en este (módulo de eliminación masiva 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),
}

Justificación variada:

  • Esto requiere TryContinue para su tipo de argumento para que el tipo de una expresión x? sea ​​siempre el mismo
  • Esto establece por defecto el parámetro de tipo en Self para casos simples como try_fold que inspeccionan y devuelven el mismo tipo, por lo que están bien con solo T: Try .
  • Este rasgo se extiende a TryContinue porque si un tipo usado como tipo de retorno permite ? en su cuerpo, entonces también debería admitir el ajuste ok.
  • La nueva enumeración es isomórfica para resultar, pero evita hablar en términos de "errores" como se discutió anteriormente, y como resultado, creo que proporciona un significado muy claro para cada una de las variantes.
  • El argumento de ? es el tipo genérico de modo que, como TryContinue , "produce un Self de algo"

Demostración completa de prueba de concepto, incluidas macros para try{} / ? e impls para Option , Result y Ordering : https : //play.rust-lang.org/? gist = 18663b73b6f35870d20fd172643a4f96 & version = stable (gracias @nikomatsakis por hacer el original de estos hace un año 🙂)

Desventajas:

  • Todavía tiene los parámetros de tipo fantasma en el Result impl que realmente no me gustan

    • Pero tal vez sea una buena compensación por no necesitar un tipo adicional LessOrGreater en el implícito Ordering .

    • Y los parámetros de tipo fantasma en las impls son menos complicados que en los tipos de todos modos

  • No da una transformación From consistente

    • Pero eso podría ser bueno, ya que a StrandResult no le importaba de todos modos, para algo como SearchResult sería extraño ya que la ruta de ruptura es la ruta del éxito y podría terminar ayudando a la inferencia en los casos anidados ? todos modos.

  • No es tan obvio cuál sería la sintaxis de throw

    • Que pierde la explicación de ? como Ok(x) => x, Err(r) => throw e.into() que realmente me gustó

    • Pero también podría permitir que throw; sea ​​la forma de producir None (a través de algo como impl<T> Throw<()> for Option<T> ), que es mucho mejor que throw NoneError;

    • Y separar eso podría ser bueno de todos modos, ya que throw LessOrGreater::Less habría sido _realmente_ tonto.

Editar: Ping @glaebhoerl , cuya opinión me gustaría sobre esto como un gran participante en RFC 1859.

Edición 2: también cc @ colin-kiegel, para esta declaración de https://github.com/rust-lang/rfcs/pull/1859#issuecomment -287402652:

Me pregunto si el enfoque esencialista podría adoptar algo de la elegancia de los reduccionistas sin sacrificar los objetivos anteriores.

Me gusta mucho esa propuesta.

No es tan obvio cuál sería la sintaxis de lanzamiento

Ignorando el bagaje de la palabra clave throw de otros lenguajes, "throw" tiene sentido, en el sentido de que está "arrojando" el valor a un ámbito superior. Python y scala también usan excepciones para el flujo de control además de los casos de error (aunque, en el caso de scala, normalmente no usaría try / catch directamente), por lo que hay un precedente para usar throw para rutas exitosas.

@tmccombs

Ignorando el bagaje de la palabra clave throw de otros lenguajes, "throw" tiene sentido, en el sentido de que está "arrojando" el valor a un ámbito superior.

Aunque viene con un equipaje similar, sospecho que "subir" encaja mejor:

lanzar (v): poner o hacer ir o entrar en algún lugar, posición, condición, etc., como lanzando:

subir (v): moverse a una posición más alta; levantar; elevar

Puede haber una manera de combinar "subir" con la lógica de ? , dado que subir también puede significar "cobrar". Algo como: Ok(v) => v, Err(e) => raise From::from(e) , donde raise imita el patrón coincidente (por ejemplo, dado un patrón Err(e) es magia sintáctica por ControlFlow::Break(Err(...)) ).

Sospecho que "subir" encaja mejor

buen punto

@scottmcm

No es tan obvio cuál sería la sintaxis de lanzamiento

¿Hay alguna razón por la que no podamos tener from_err(value: Other) ?

ACTUALIZACIÓN: Quizás estoy confundido sobre el papel de Other , en realidad. Tengo que estudiar este rasgo un poco más. =)

@nikomatsakis

¿Hay alguna razón por la que no podamos tener from_err(value: Other) ?

Bueno, Other es un tipo ? -able completo (como Result ), por lo que no quisiera que throw Ok(4) funcione. (Y debe ser la disyunción completa para evitar forzar la introducción de un tipo de error artificial). Por ejemplo, creo que nuestra interoperabilidad Opción-Resultado actual sería equivalente a esto (y al 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))),
        }
    }
}

Mi inclinación actual por throw sería por esto:

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

con

throw $expr  ==>  break 'closest_catch TryBreak::from_break($expr)
  • Extiende TryContinue para que el ajuste correcto también esté disponible para el tipo de retorno del método en el que se usa
  • No es un tipo asociado, por lo que podría implicar ambos, digamos, TryBreak<TryFromIntError> y TryBreak<io::Error> para un tipo si lo desea.

    • y impl<T> TryBreak<()> for Option<T> para habilitar solo throw; siente plausible de una manera que un tipo de error asociado de () no lo hizo.

    • impl<T> TryBreak<!> for T estaría bien, pero probablemente sea incoherente.

(Aparte: estos nombres de métodos y características son terribles; ¡ ayúdenos !)

Mis pensamientos sobre las otras cuestiones planteadas aquí no se han concretado todavía en una forma fácilmente articulable, pero con respecto a los problemas de inferencia de tipos en torno al From::from() desugaring (no recuerdo si esto también se discutió en otro lugar recientemente , ¿o solo aquí?):

El sentido instintivo (de la experiencia de Haskell) de que "de esa manera se encuentran las ambigüedades de inferencia de tipo" fue una de las razones (aunque no la principal) por las que no quería tener una conversión From como parte del RFC original. Ahora que está horneado en el pastel, sin embargo, me pregunto si no podríamos intentar tener ese pastel y comérnoslo también "simplemente" agregando una regla especial por defecto al proceso de inferencia de tipos.

Es decir, creo: siempre que veamos un From::from() [opcionalmente: uno que fue insertado por el ? desugaring, en lugar de escrito manualmente], y sabemos exactamente uno de los dos tipos (input vs salida), mientras que el otro es ambiguo, por defecto el otro es el mismo que el anterior. En otras palabras, creo que cuando no está claro qué impl From usar, por defecto es impl<T> From<T> for T . Creo que esto es básicamente siempre lo que realmente quieres. Tal vez sea un poco ad-hoc, pero si funciona, los beneficios parecen valer los costos en mi humilde opinión.

(También pensé que From ya era un elemento de lang, precisamente debido al ? desugaring, pero no parece serlo? En cualquier caso, ya es de alguna manera especial por esa razón .)

en la propuesta de @scottmcm , From::from() _no_ es parte del desugaring, sino que está en la implementación de Try por Result .

@tmccombs No estaba proponiendo una enmienda a su propuesta.

El try{} RFC (https://github.com/rust-lang/rfcs/pull/2388) ha estado discutiendo bloques try donde a uno no le importa el resultado. Esta alternativa parece manejar eso decentemente bien, ya que puede optar por ignorar los tipos de error por completo si así lo desea, lo que permite

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

Implementación de prueba de concepto con los mismos rasgos que antes: https://play.rust-lang.org/?gist=e0f6677632e0a9941ed1a67ca9ae9c98&version=stable

Creo que hay algunas posibilidades interesantes allí, especialmente porque una implementación personalizada podría, digamos, solo tomar resultados y enlazar E: Debug para que automáticamente registre cualquier error que ocurra. O se podría hacer una versión destinada específicamente como un tipo de retorno para main junto con Termination que "simplemente funciona" para permitirle usar ? sin una firma de tipo compleja (https : //github.com/rust-lang/rfcs/issues/2367).

Tuve problemas similares a los evidenciados por @nikomatsakis usando la versión existente del rasgo Try . Para conocer los problemas específicos, consulte https://github.com/SergioBenitez/Rocket/issues/597#issuecomment -381533108.

Las definiciones de rasgos propuestas por @scottmcm resuelven estos problemas. Sin embargo, parecen ser más complicados de lo necesario. Intenté volver a implementarlos y se me ocurrió lo siguiente:

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

El cambio principal es que el tipo asociado Continue está en el rasgo Try en lugar del FromTry (anteriormente TryContinue ). Además de simplificar las definiciones, esto tiene la ventaja de que Try se puede implementar independientemente de FromTry , lo que considero más común, y que la implementación de FromTry se simplifica una vez Try está implementado. (Nota: si se desea que Try y FromTry se implementen al unísono, simplemente podemos mover el método from_try a Try )

Vea el campo de juegos completo con implementaciones para Result y Option , así como los Outcome Rocket en este campo de juegos .

¡Gracias por el informe, @SergioBenitez! Esa implementación coincide con la versión alternativa de "cambiar los parámetros de tipo" de la propuesta de rasgo original en RFC 1859: https://github.com/rust-lang/rfcs/blob/f89568b1fe5db4d01c4668e0d334d4a5abb023d8/text/0000-try-trait.md#unresolved -preguntas

Lo más grande que pierde es la propiedad de que typeof(x?) depende solo de typeof(x) . La falta de esa propiedad era una de las preocupaciones con el original ("Me preocupa un poco la legibilidad del código en este sentido" https://github.com/rust-lang/rfcs/pull/1859#issuecomment-279187967) y una ventaja de la propuesta reduccionista final ("Para cualquier tipo T dado,? puede producir exactamente un tipo de valor ok / error" https://github.com/rust-lang/rfcs/pull/1859#issuecomment-283104310). Por supuesto, también hubo argumentos de que dicha propiedad es innecesaria o demasiado restrictiva, pero en el resumen final antes de FCP todavía estaba allí como una ventaja (https://github.com/rust-lang/rfcs/pull/1859#issuecomment -295878466).

Además de simplificar las definiciones, esto tiene la ventaja de que Try se puede implementar independientemente de FromTry , lo que considero más común.

Ciertamente, hoy en día from_ok es menos común, ya que Try y do catch son inestables. E incluso si do catch fuera estable, estoy de acuerdo en que se usará menos de ? general (ya que la mayoría de estos bloques contienen múltiples ? s).

Sin embargo, desde la perspectiva del rasgo y sus operaciones, creo que "envolver un valor 'continuo' en el tipo de portador" es una parte absolutamente esencial de ? . Hoy en día, rara vez se pasa por el rasgo, solo se usa Ok(...) o Some(...) , pero es fundamental para el uso genérico. Por ejemplo, try_fold :

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

Si una función va a devolver un tipo de operador, es fundamental que haya una manera de poner el valor de interés en ese tipo de operador. Y, lo que es más importante, no creo que proporcionar esto sea una dificultad. Tiene una definición muy natural de Resultado, Opción, Resultado, etc.

@Centril también ha señalado antes que "envolver un escalar en un portador" también es una construcción más simple en teoría. Por ejemplo, Haskell lo llama la clase de tipo Pointed , aunque no creo que queramos generalizarlo _ tan lejos en Rust: permitir try { 4 }vec![4] parece una exageración .

También estoy imaginando un futuro en el que, como se proponen las funciones async para envolver el valor del bloque en un futuro, podríamos tener funciones try que envuelvan el valor del bloque en un tipo falible. Allí, de nuevo, TryContinue es la parte crítica; tal función podría ni siquiera usar ? , si obtuvimos una construcción throw .

Así que todo eso es, lamentablemente, más filosófico que concreto. Déjame saber si tiene algún sentido, o si piensas lo contrario en alguna de las partes: levemente_sonriente_cara:

Editar : disculpas si recibiste un correo electrónico con una versión anterior de esto; Presioné "comentar" demasiado pronto ...

Lo más grande que pierde es la propiedad de que typeof (x?) Depende solo de typeof (x).

Ah, sí, absolutamente. Gracias por señalar eso.

Por supuesto, también hubo argumentos de que dicha propiedad es innecesaria o demasiado restrictiva, pero en el resumen final antes de FCP todavía estaba ahí como una ventaja (rust-lang / rfcs # 1859 (comentario)).

¿Hay ejemplos específicos de dónde podría ser demasiado restrictivo?

Si una función va a devolver un tipo de operador, es fundamental que haya una manera de poner el valor de interés en ese tipo de operador. Y, lo que es más importante, no creo que proporcionar esto sea una dificultad.

Creo que es un análisis justo. Estoy de acuerdo.

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

Entonces, la pregunta es, ¿son suficientes las restricciones propuestas? ¿Hay buenos usos del contexto del caso de error para determinar el tipo de éxito? ¿Hay posibles abusos?

Puedo decir por mi experiencia en el futuro que bien puede haber algunos casos útiles aquí. En particular, el tipo de Encuesta del que habla se puede procesar de dos maneras. A veces, queremos saltar a la variante NotReady y quedarnos esencialmente con un valor de Resultado. A veces, solo nos interesa la variante Ready y queremos saltar sobre cualquiera de las otras variantes (como en su boceto). Si permitimos que el tipo de éxito sea determinado en parte por el tipo de error, es más plausible apoyar ambos casos y, básicamente, usar el contexto para determinar qué tipo de descomposición se desea.

OTOH, me preocupa un poco la legibilidad del código en este sentido. Esto se siente cualitativamente diferente a simplemente convertir el componente de error, ¿significa que el básico coincide con eso? el rendimiento depende de la información contextual.

Entonces, uno podría imaginar un rasgo que moviera _ambos_ tipos a parámetros de tipo, como

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

Y use que habilite todos los

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

Pero creo que definitivamente es una mala idea, ya que significa que estos no funcionan

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

Dado que no hay un contexto de tipo para decir qué producir.

La otra versión sería habilitar cosas 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()?;
}

Mi instinto es que la inferencia "fluye fuera del?" hay demasiado sorprendente, y por lo tanto esto está en el cubo "demasiado inteligente".

Críticamente, no creo que no tenerlo sea demasiado restrictivo. my_result? en una función -> Poll no lo necesita, ya que el tipo de éxito es el mismo que de costumbre (importante para mantener el código síncrono funcionando igual en contextos asíncronos) y la variante de error también se convierte bien . Usar ? en un Poll en un método que devuelve Result parece un anti-patrón de todos modos, no algo que debería ser común, así que usar métodos dedicados (hipotéticos) como .wait(): T (para bloquear el resultado) o .ready(): Option<T> (para comprobar si está hecho) para elegir el modo es probablemente mejor de todos modos.

Esto es "interesante" https://play.rust-lang.org/?gist=d3f2cd403981a631f30eba2c3166c1f4&version=nightly&mode=debug

No me gustan estos bloques de prueba (atrapan), no parecen muy amigables para los recién llegados.

Estoy tratando de recopilar el estado actual de la propuesta, que parece extenderse a través de múltiples comentarios. ¿Existe un único resumen del conjunto de características propuesto actualmente (que elimina el tipo asociado Error )?

Al principio de este hilo, veo un comentario sobre la división de Try en rasgos de error separados a / desde: ¿hay algún plan para implementar esa división?

Sería útil tener una conversión transparente de Result<T, E> a cualquier tipo Other<T2, E> en el signo de interrogación; esto permitiría llamar a las funciones IO existentes con una sintaxis agradable desde una función con un aspecto más especializado (p. Ej. perezoso) 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(())
    })
}

Semánticamente, esto se siente como From::from(E) -> Other<T2, E> , pero el uso de From está restringido actualmente a la implementación existente Result -equivalente Try .

Realmente creo que NoneError debería tener un problema de seguimiento por separado. Incluso si el rasgo Try nunca se estabiliza, NoneError debería estabilizarse porque hace que usar ? en Option mucho más ergonómico. Considere esto para errores como struct MyCustomSemanthicalError; o errores al implementar Default . None podría convertirse fácilmente en MyCustomSeemanthicalError través de From<NoneError> .

Al trabajar en https://github.com/rust-analyzer/rust-analyzer/ , me encontré con un papercut ligeramente diferente de las insuficiencias en el operador ? , particularmente cuando el tipo de retorno es Result<Option<T>, E> .

Para este tipo, tiene sentido que ? desugar efectivamente para:

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

donde value es de tipo Result<Option<V>, E> , o:

value?

donde value es Result<V, E> . Creo que esto es posible si las bibliotecas estándar implementan Try de la siguiente manera para Option<T> , aunque no lo he probado explícitamente y creo que puede haber problemas de especialización.

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

Cuando le preguntó a @matklad por qué no podía crear una enumeración personalizada implementando Try , que se llamaría Cancellable en este caso, señaló que std::ops::Try es inestable, por lo que no se puede usar de todos modos dado que rust-analyzer (actualmente) apunta al óxido estable.

Vuelva a publicar desde https://github.com/rust-lang/rust/issues/31436#issuecomment -441408288 porque quería comentar sobre esto, pero creo que ese era el problema equivocado:

Esencialmente, una situación que tengo son devoluciones de llamada en un marco de GUI: en lugar de devolver un Option o Result , deben devolver un UpdateScreen , para decirle al marco si la pantalla necesita ser actualizado o no. A menudo no necesito iniciar sesión en absoluto (simplemente no es práctico iniciar sesión en cada error menor) y simplemente devolver un UpdateScreen::DontRedraw cuando se ha producido un error. Sin embargo, con el operador actual ? , tengo que escribir esto todo el tiempo:

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

Como no puedo convertir de Result::Err en UpdateScreen::DontRedraw través del operador Try, esto se vuelve muy tedioso; a menudo tengo búsquedas simples en mapas hash que pueden fallar (lo cual no es un error ) - tan a menudo en una devolución de llamada tengo de 5 a 10 usos del operador ? . Debido a que lo anterior es muy detallado de escribir, mi solución actual es impl From<Result<T>> for UpdateScreen así , y luego usar una función interna en la devolución de llamada como esta:

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

Dado que la devolución de llamada se usa como un puntero de función, no puedo usar un -> impl Into<UpdateScreen> (por alguna razón, devolver un impl actualmente no está permitido para los punteros de función). Entonces, la única forma en que puedo usar el operador Try es hacer el truco de la función interna. Sería bueno si pudiera simplemente hacer algo como esto:

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
}

No estoy seguro de si esto sería posible con la propuesta actual y solo quería agregar mi caso de uso para su consideración. Sería genial si un operador Try personalizado pudiera admitir algo como esto.

@joshtriplett Perdón por tomarme un tiempo para volver a esto. He reunido un prototipo funcional de la propuesta en https://github.com/rust-lang/rust/compare/master...scottmcm : try-trait-v2 para ser concreto. Espero probar algunas cosas más con él.

@scottmcm ¿Tiene alguna explicación de nivel superior de los cambios?

@scottmcm FWIW También probé tus cambios en rayón:
https://github.com/rayon-rs/rayon/compare/master...cuviper : try-trait-v2
(todavía usando copias privadas en lugar de elementos inestables)

Entonces, ¿cuál es la solución para convertir la opción NoneError? Parece que, debido a que implementa Try Trait, no se compilará a menos que habilite el uso de esa característica en particular (¿inestable?).

Entonces, básicamente, no puedes usar el? operador con Option hasta donde yo sé?

@omarabid , el operador es estable para usar con Option o Result , pero no puede usar Try como restricción genérica hasta que sea estable. Está perfectamente bien usar ? en un Option en una función que devuelve Option , ya que no tiene que involucrar NoneError en absoluto. También puede devolver un Result si borra los 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)?)
}

( patio de recreo )

@scottmcm , ¡su prototipo try-trait-v2 falla en este ejemplo!

Si no queremos que mi ejemplo se rompa, try-trait-v2 necesitará 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))),
        }
    }
}

¿Cuál es el estado actual de esta función?

El PR # 62606 para documentar la implementación de try_fold para iteradores debe reabrirse una vez que se estabilice.

Editar: se actualizó la operación con un elemento de seguimiento para eso ~ scottmcm

¿Hay planes para reemplazar el rasgo Try con alguna de las alternativas sugeridas en este hilo? La versión sugerida por @scottmcm parece estar bien. Quiero seguir usando el operador ? con Option , y creo que el rasgo Try debería cambiarse para no forzar la semántica de Result en Option .

Usar la alternativa de @scottmcm nos permitiría usar ? con Option y deshacernos de NoneError . Estoy de acuerdo con @nikomatsakis ( comentario ) en que el rasgo Try no debería promover la necesidad de "definir artificialmente un tipo para representar el 'fallo' de un Option ".

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

Principiante aquí, fui un poco terco al querer conseguirlo? para escribir automáticamente borrar tanto cualquier Error como Opción.
Después de pasar demasiado tiempo tratando de entender por qué otras soluciones probables no se pueden implementar, descubrí que @cuviper es lo más cercano a lo que puedo obtener.
Algunas explicaciones habrían ayudado, pero al menos pude familiarizarme de cerca con algunas limitaciones de la metaprogramación de Rust.
Así que traté de resolverlo y explicarlo en términos concretos.
Este hilo parece la encrucijada más probable en la que espero poder ayudar a cualquiera como yo que se encuentre con esto, no dude en corregirlo:

  • Usando Debug (común a StdError y NoneError) en lugar de StdError:
    Un conflicto From<T: StdError> for Error genérico y un From<NoneError> for Error
    Porque sería ambiguo si NoneError implementara StdError (aparentemente, incluso el valor predeterminado no permite anulaciones y aplica la exclusividad)
    Esto podría resolverse con un "límite negativo" que no es compatible, ¿quizás porque sería el único caso de uso? (sobre especialización)
    impl StdError para NoneError solo se puede definir junto con StdError o NoneError, de modo que sea coherente en cualquier sentido descendente.
  • El error no puede ser un alias:
    type Error = Box<Debug> vincula Debug, lo que hace que From<T:Debug> for Error conflicto con From<T> for T (De reflexivo para idempotencia)

Entonces, debido a que Error no puede implementar la depuración, es posible que desee tener un ayudante para desenvolverlo en un resultado con depuración transitiva:

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

No puede ser impl From<Result<T>> for StdResult<T> ni Into<StdResult> for Result<T> (no puede implicar un rasgo ascendente para el tipo ascendente)

Por ejemplo, puede usarlo para obtener una devolución de depuración por terminación:

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

Idea maligna: tal vez el operador ? podría representar de alguna manera un enlace monádico, por lo que

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

se convierte en

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

Una idea terrible, pero agrada a mi friki interior.

@derekdreery No creo que eso funcione bien con un flujo de control imperativo como return y continue

Tenga en cuenta que la semántica del operador ? ya es estable. Solo el rasgo Try real es inestable, y cualquier cambio debe preservar el mismo efecto estable para Option y Result .

@KrishnaSannasi

@derekdreery No creo que eso funcione bien con un flujo de control imperativo como regresar y continuar

Concuerdo con esta declaración.

@cuviper

Tenga en cuenta que la semántica de? El operador ya está estable. Solo el rasgo Try real es inestable, y cualquier cambio debe preservar el mismo efecto estable para Option y Result.

¿Es esto cierto también a lo largo de las épocas?

En una nota más general, no creo que sea posible unificar conceptos como .await , ?/ devolución anticipada, Option :: map, Result :: map, Iterator :: map en rust, pero entender que todos comparten alguna estructura definitivamente me ayuda a ser un mejor programador.

Disculpas por ser OT.

No estoy seguro de si una época / edición podría cambiar ? desugaring, pero eso sin duda complicaría muchas preocupaciones cruzadas como las macros.

Mi interpretación de las garantías de estabilidad y las épocas RFC es que el comportamiento de ? (azúcar o de otro tipo) podría cambiarse, pero su comportamiento actual en los tipos de Option / Result debe permanecer el Lo mismo porque romper eso crearía mucha más deserción de la que podríamos esperar justificar.

¿Qué pasa si Option alguna manera fuera un alias de tipo para Result ? Es decir, type Option<T> = Result<T, NoValue> , Option::<T>::Some(x) = Result::<T, NoValue>::Ok(x) , Option::<T>::None = Result::<T, NoValue>::Err(NoValue) ? Eso requeriría algo de magia para implementarlo y no sería realista en el entorno del lenguaje actual, pero tal vez valga la pena explorarlo.

No podemos hacer este cambio porque hay rasgos implícitos que dependen de que Option an Result sean tipos distintos.

Si ignoramos eso, entonces podríamos dar un paso más e incluso hacer que bool un alias para Result<True, False> , donde True y False son tipos de unidades.

¿Se ha considerado agregar un Try impl por unidad, () ? Para las funciones que no devuelven nada, una devolución anticipada aún puede ser útil en el caso de un error en una función no crítica, como una devolución de llamada de registro. ¿O se excluyó la unidad porque se prefiere nunca ignorar silenciosamente los errores en cualquier situación?

¿O se excluyó la unidad porque se prefiere nunca ignorar silenciosamente los errores en cualquier situación?

Esta. Si desea ignorar los errores en un contexto no crítico, debe usar unwrap o una de sus variantes.

ser capaz de usar ? en algo como foo() -> () sería bastante útil en un contexto de compilación condicional y debería considerarse seriamente. Sin embargo, creo que es diferente de tu pregunta.

debe usar desenvolver o una de sus variantes.

unwrap() causa pánico, por lo que no recomendaría usarlo en contextos no críticos. No sería apropiado en situaciones en las que se desea una simple devolución. Se podría argumentar que ? no es totalmente "silencioso" debido al uso explícito y visible de ? en el código fuente. De esta manera, ? y unwrap() son análogos para las funciones unitarias, solo que con una semántica de retorno diferente. La única diferencia que veo es que unwrap() hará visibles los efectos secundarios (abortar el programa / imprimir un stacktrace) y ? no.

En este momento, la ergonomía del regreso temprano en las funciones de la unidad es considerablemente peor que en las que regresan Result o Option . Quizás este estado de cosas sea deseable porque los usuarios deberían usar un tipo de retorno de Result o Option en estos casos, y esto les proporciona un incentivo para cambiar su API. De cualquier manera, puede ser bueno incluir una discusión sobre el tipo de devolución de la unidad en el RP o en la documentación.

ser capaz de usar ? en algo como foo() -> () sería bastante útil en un contexto de compilación condicional y debería considerarse seriamente. Sin embargo, creo que es diferente de tu pregunta.

¿Cómo funcionaría esto? ¿Simplemente evalúe siempre a Ok (()) y sea ignorado?

Además, ¿puede dar un ejemplo de dónde le gustaría usar esto?

Sí, la idea era que algo como MyCollection :: push podría, dependiendo de la configuración del tiempo de compilación, tener un valor de retorno Result <(), AllocError> o un valor de retorno () si la colección está configurada para entrar en pánico en caso de error. El código intermedio que usa la colección no debería tener que pensar en eso, por lo que si pudiera simplemente _siempre_ usar ? incluso cuando el tipo de retorno es () , sería útil.

Después de casi 3 años, ¿está esto más cerca de resolverse?

@Lokathor eso solo funcionaría si un tipo de retorno Result<Result<X,Y>,Z> o similar no fuera posible. Pero es. Por tanto, no es posible.

No entiendo por qué un resultado en capas causa problemas. ¿Podrías dar más detalles?

Para fines de referencias cruzadas, @dureuill ha propuesto una formulación alternativa en los

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

@Lokathor
Ok, lo pensé más profundamente y creo que podría haber encontrado una explicación bastante buena.

Usando una anotación

El problema es la interpretación del tipo de retorno o las anotaciones extrañas saturarían el código. Sería posible, pero dificultaría la lectura del código. (Condición previa: #[debug_result] aplica su comportamiento deseado y modifica una función para que entre en pánico en el modo de liberación en lugar de devolver un Result::Err(...) , y desenvuelve Result::Ok , pero esa parte es 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)
}

Por lo tanto, dificultaría la lectura del código.
No podemos simplemente modificar el comportamiento de ? simplemente a una operación no operativa,
especialmente si el valor de retorno de un #[debug_result] se guarda en una variable temporal y luego se "intenta propagar" con el operador ? . Haría la semántica de ? realmente confusa, porque dependería de muchas "variables", que no son necesariamente conocidas en el "momento de escribir" o podrían ser difíciles de adivinar simplemente leyendo el código fuente. #[debug_result] necesitaría estropear las variables a las que se les asignan valores de función, pero no funcionará si una función está marcada con #[debug_result] y otra no, por ejemplo, lo siguiente sería un error 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 un nuevo tipo

Una solución "más limpia" podría ser un tipo DebugResult<T, E> que simplemente entra en pánico cuando se establece un determinado indicador de compilación y se construye a partir de un error, pero de lo contrario sería equivalente a Result<T, E> . Pero eso también sería posible con la propuesta actual, creo.

Respuesta a la última publicación de @zserik : La macro que describió no tiene sentido: el tipo de función de retorno de carga basado en la configuración de compilación romperá todas las coincidencias en el resultado de la función en todas las configuraciones de compilación posibles, excepto la única, independientemente de la forma en que se hizo.

@ tema3210 Lo sé. Solo quería señalar que la idea de @Lokathor generalmente no funcionaría en la práctica. Lo único que podría funcionar parcialmente es lo siguiente, pero solo con muchas restricciones, que no creo que valga la pena a largo plazo:

// 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 ¿Es posible que realmente adopte una 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, buena idea.

No estoy realmente seguro de si esto es algo que deba tenerse en cuenta antes de la estabilización, pero estoy interesado en implementar trazas de retorno de error y creo que implica cambios en el rasgo Try o al menos en su impl proporcionado por Result para que funcione. Eventualmente planeo convertir esto en un RFC, pero quiero asegurarme de que el rasgo de prueba no se estabilice de una manera que haga que sea imposible agregarlo más adelante en caso de que me lleve un tiempo escribir dicho RFC. La idea básica es esta.

Tiene un rasgo que usa para pasar información de seguimiento en el que usa la especialización y una implicación predeterminada para que T mantenga la compatibilidad con versiones 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>) {}
}

Y luego modifica la implementación Try para Result para usar track_caller y pasar esta información al tipo interno,

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

Y luego, para los tipos que desea recopilar rastreos, implemente 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);
    }
}

El uso termina luciendo así

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

Y el resultado de una versión muy mala de un backtrace se ve así

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

Y aquí hay una prueba de concepto en acción https://github.com/yaahc/error-return-traces

Pensé que solo un tipo de error que podemos convertir para probar el implementador de rasgos podría ser insuficiente, por lo que podríamos proporcionar una interfaz 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;
}

Tenga en cuenta que el compilador puede monomorfizar from_error , evitando From::from call, y uno puede proporcionar el método impl para diferentes tipos de error manualmente, lo que da como resultado la capacidad del operador ? para "desenvolver" diferentes tipos de errores.

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

Como está escrito, el implementador tendría que aceptar _cualquier_ tamaño T , sin restricciones. Si deseaba permitir que esto se restringiera de manera personalizada, tendría que ser un parámetro de rasgo, como Try<T> . Esto es similar a la combinación TryBlock / Bubble<Other> que tenía @scottmcm en https://github.com/rust-lang/rust/issues/42327#issuecomment-457353299 .

Como está escrito, el implementador tendría que aceptar _cualquier_ tamaño T , sin restricciones. Si deseaba permitir que esto se restringiera de manera personalizada, tendría que ser un parámetro de rasgo, como Try<T> . Esto es similar a la combinación TryBlock / Bubble<Other> que @scottmcm tenía en # 42327 (comentario) .

Quise decir que el uso debería verse así:

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

necesitaría dividir Try y TryFromError. Me gusta eso más que la propuesta original, pero creo que necesitaría un nuevo RFC.

(y sigo pensando que debería haberse llamado "propagar" en lugar de "intentar", pero estoy divagando)

@ tema3210 Creo que entiendo tu intención, pero eso no es válido para Rust.

@ SoniEx2

necesitaría dividir Try y TryFromError.

Bien, por eso mencioné el diseño TryBlock / Bubble<Other> . Podemos debatir esa elección de nombre, pero la idea era que el retorno temprano no siempre se trata de _errores_, per se. Por ejemplo, muchos de los métodos internos Iterator utilizan un tipo LoopState . Para algo como find , no es un error encontrar lo que está buscando, pero queremos detener la iteración allí.

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

Podemos debatir esa elección de nombres, pero la idea era que el retorno temprano no siempre se trata de errores, per se.

precisamente por qué no me gusta el nombre "probar" y preferiría el nombre "propagar", porque "propaga" un retorno "temprano": p

(¿No estoy seguro de si esto tiene sentido? La última vez que mencioné esto, lo de "propagar" solo tenía sentido para mí por alguna razón y nunca fui capaz de explicárselo a los demás).

¿Este rasgo será de alguna ayuda al intentar sobrescribir el comportamiento predeterminado ? fe para agregar un gancho de registro fe para registrar información de depuración (como el número de archivo / línea)?

Actualmente es compatible para sobrescribir macros stdlib, pero parece que el operador ? no se convierte explícitamente en la macro try! . Eso es lamentable.

@stevenroose Para agregar soporte para eso únicamente al rasgo Try , se requeriría una modificación del rasgo Try para incluir información de ubicación del archivo sobre la ubicación donde ? "sucedió" .

@stevenroose Para agregar soporte para eso únicamente al rasgo Try , se requeriría una modificación del rasgo Try para incluir información de ubicación del archivo sobre la ubicación donde ? "sucedió" .

Esto no es cierto, # [track_caller] se puede usar en los rasgos implícitos incluso si la definición del rasgo no incluye la anotación

@stevenroose para responder a su pregunta, sí, aunque si está interesado en imprimir todas las ubicaciones de ? través de las que se propaga un error, debe consultar el comentario de seguimiento de devolución de error de arriba

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

No estoy seguro de si alguien ya ha mencionado esto, ¿deberíamos impl Try for bool , tal vez Try<Ok=(), Error=FalseError> ?
Para que podamos hacer algo como esto

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

Ahora tengo que usar el tipo de retorno Option<()> en la mayoría de los casos en los que creo que ? podría hacer que el código sea mucho más claro.

No estoy seguro de si alguien ya ha mencionado esto, ¿deberíamos impl Try for bool , tal vez Try<Ok=(), Error=FalseError> ?

Esto haría que Try comportara como el operador && en bool s. Como otros han señalado anteriormente, también hay casos de uso de cortocircuitos en caso de éxito, en cuyo caso Try debería comportarse como || . Dado que los operadores && y || no son mucho más largos para escribir, tampoco veo muchas ventajas en tener esta implementación.

@calebsander gracias por la amable respuesta.
Eso es cierto para algunos casos simples, pero no creo que sea el caso a menudo, especialmente nunca podríamos tener una declaración de asignación como let x = v.get_mut(key)? en la expresión.
Si && y || siempre funcionan, también podríamos jugar con .unwrap_or_else() , .and_then() por Option y Error casos.
¿Podría expresar el código de flujo en && y || ?

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 alguna condición en la que true significa fallido mientras que false significa éxito, !expr? puede ser confuso, pero podríamos usar expr.not()? para hacer el truco (nota: para ops::Try , siempre tenemos false por Error )

Eso es cierto para algunos casos simples, pero no creo que sea el caso a menudo, especialmente nunca podríamos tener una declaración de asignación como let x = v.get_mut(key)? en la expresión.

Bueno, a menos que no entienda bien tu propuesta, simplemente implementar Try<Ok = (), Error = FalseError> por bool no permitiría esto. También necesitaría impl From<NoneError> for FalseError para que el operador ? pueda convertir None en false . (Y de manera similar, si desea aplicar ? a un Result<T, E> dentro de una función que devuelve bool , necesitaría impl From<E> for FalseError . Creo que tal la implementación general sería problemática). También puede usar some_option().ok_or(false)? y some_result().map_err(|_| false)? si está de acuerdo con el valor de retorno Result<bool, bool> (que puede colapsar con .unwrap_or_else(|err| err) ).

Dejando de lado los problemas de convertir otros Try errores en bool , la implementación Try que está proponiendo para bool es solo el operador && . Por ejemplo, estos son equivalentes

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

y

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

(Es cierto que los flujos de control como if y loop que devuelven no () son menos sencillos de traducir).

Para alguna condición en la que true significa fallido mientras que false significa éxito, !expr? puede ser confuso, pero podríamos usar expr.not()? para hacer el truco (nota: para ops::Try , siempre tenemos false por Error )

No me queda claro que false deba representar el caso Error . (Por ejemplo, Iterator.all() querría false en cortocircuito, pero Iterator::any() querría true lugar). comportamiento de cortocircuito opuesto invirtiendo el valor pasado a ? (e invirtiendo también el valor de retorno de la función). Pero no creo que eso dé como resultado un código muy legible. Podría tener más sentido tener struct s And(bool) y Or(bool) separados que implementen los dos comportamientos diferentes.

Y de manera similar, ¿quieres postularte? a un resultadodentro de una función que devuelve bool, necesitaría implicar Frompara FalseError. Creo que una implementación tan generalizada sería problemática.

No, no quiero impl From<T> for FalseError , tal vez podríamos hacer result.ok()?

No me queda claro que falso deba representar el caso de Error.

Creo que de alguna manera es natural cuando tenemos bool::then que mapean true a Some .

Perdóname si me lo perdí, ¿por qué no NoneError impl std::error::Error ? Esto lo hace inútil para cualquier función que devuelva Box<dyn Error> o similar.

Perdóname si me lo perdí, ¿por qué no NoneError impl std::error::Error ? Esto lo hace inútil para cualquier función que devuelva Box<dyn Error> o similar.

No soy un experto aquí, pero puedo ver problemas importantes al intentar generar un mensaje de error útil para "algo era None y esperabas que fuera Some " (que es básicamente lo que estaría ganando aquí - algún mensaje de diagnóstico). En mi experiencia, siempre ha tenido más sentido usar el combinador Option::ok_or_else para usar otro tipo de error en su lugar, porque como código de llamada generalmente tengo un contexto mucho mejor para dar de todos modos.

Estoy de acuerdo con @ErichDonGubler. Es muy molesto no poder usar ? con Option , sin embargo, es por una buena razón y, como usuario de un idioma, creo que lo mejor para todos es manejar los errores correctamente. Hacer este trabajo adicional de vincular el contexto de error a None lugar de simplemente hacer ? es realmente muy molesto, pero obliga a comunicar correctamente los errores, lo cual vale la pena.

La única vez que permitiría que None se interprete como un error es en la creación de prototipos muy aproximada. Supuse que si agregamos algo como una llamada Option::todo_err() , eso devolvería Ok del valor interno, pero entraría en pánico en None . Es muy contrario a la intuición hasta que se analizan las necesidades del modo de creación de código de "creación de prototipos aproximados". Es muy similar a Ok(my_option.unwrap()) , pero no necesita un envoltorio Ok(...) . Además, especifica explícitamente la naturaleza "todo", comunicando así la intención de eliminar este código de la lógica de producción, reemplazándolo con un enlace de error adecuado.

pero entrará en pánico en Ninguno

Siento firmemente que deberíamos dejar esto en unwrap . Si agregamos un todo_err, debería devolver un tipo de error real, lo que plantea la pregunta de qué tipo de error devolver.

También sospecho que fn todo_err(self) -> Result<Self, !> tendría el problema obvio de requerir ! para estabilizarse, lo cual uh, bueno, Algún día.

Mi caso de uso es de hecho un prototipo aproximado en el que no me preocupan demasiado los errores. ¿No sufre todo el NoneError estos problemas que mencionaste? Si se decide que debería existir (lo que creo que es algo bueno, al menos para la creación de prototipos), creo que debería implicar Error ya que se denomina como uno.

Debido al tipo que carece de esta implícita, recurrí a simplemente pegarle un .ok_or("error msg") , que también funciona pero es menos conveniente.

¿Fue útil esta página
0 / 5 - 0 calificaciones