Rust: Problema de seguimiento para RFC 1937: `?` en `principal`

Creado en 18 jul. 2017  ·  183Comentarios  ·  Fuente: rust-lang/rust

Este es un problema de seguimiento para el RFC " ? en main " (rust-lang/rfcs#1937).

Pasos:

Estabilizaciones:

  • [x] Estabilizar main con tipos de retorno que no sean () (https://github.com/rust-lang/rust/issues/48453) Fusionado en https://github.com/rust-lang/ óxido/tirar/49162
  • [x] Estabilizar las pruebas unitarias con tipos de devolución no () (https://github.com/rust-lang/rust/issues/48854)

Asuntos relacionados:

  • [x] El mensaje de error para las pruebas unitarias no es excelente (https://github.com/rust-lang/rust/issues/50291)

Preguntas sin resolver:

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

Comentario más útil

Disculpas por intervenir en un punto en el que probablemente sea demasiado tarde para hacer algo al respecto, pero quería dejar mis comentarios aquí en caso de que los hubiera. Leí la mayor parte de este hilo, así que hablo con ese contexto en mente. Sin embargo, este hilo es largo, por lo que si parece que he pasado por alto algo, entonces probablemente lo hice y agradecería que me lo indicaran. :-)

TL; DR: creo que mostrar el mensaje de error Debug fue un error, y que una mejor opción sería usar el mensaje de error Display .

En el centro de mi creencia es que, como alguien que _rutinariamente crea programas CLI en Rust_, no recuerdo haberme preocupado mucho por cuál es el mensaje Debug de un Error . Es decir, el Debug de un error es, por diseño, para los desarrolladores, no para los usuarios finales. Cuando crea un programa CLI, su interfaz está fundamentalmente destinada a ser leída por los usuarios finales, por lo que un mensaje Debug tiene muy poca utilidad aquí. Es decir, si algún programa CLI que envié a los usuarios finales mostrara la representación de depuración de un valor de Rust en funcionamiento normal, entonces consideraría que se ha corregido un error. En general, creo que esto debería ser cierto para todos los programas CLI escritos en Rust, aunque entiendo que puede ser un punto en el que las personas razonables pueden estar en desacuerdo. Dicho esto, una implicación un tanto sorprendente de mi opinión es que hemos estabilizado efectivamente una función en la que su modo de operación predeterminado lo inicia con un error (nuevamente, en mi opinión) que debe corregirse.

Al mostrar la representación Debug de un error de forma predeterminada, también creo que estamos fomentando una mala práctica. En particular, es muy común en el curso de escribir un programa Rust CLI observar que incluso el Display impl de un error no es lo suficientemente bueno para ser consumido por los usuarios finales, y que se debe hacer un trabajo real para arreglalo. Un ejemplo concreto de esto es io::Error . Mostrar un io::Error sin una ruta de archivo correspondiente (asumiendo que proviene de leer/escribir/abrir/crear un archivo) es básicamente un error, porque es difícil para un usuario final hacer algo con él. Al elegir mostrar la representación Debug de un error de forma predeterminada, hemos hecho más difícil que la gente que crea programas CLI descubra ese tipo de errores. (Además de eso, el Debug de un io::Error es mucho menos útil que su Display , pero eso por sí solo no es un gran problema en mi experiencia. )

Finalmente, para completar mi argumento, también me cuesta imaginar las circunstancias bajo las cuales usaría ?-in-main incluso en ejemplos. Es decir, he estado tratando de escribir ejemplos que coincidan lo más posible con programas del mundo real, y esto generalmente ha implicado escribir cosas como esta:

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

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

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

A primera vista, sería _encantador_ reemplazar esto con ?-in-main , pero no puedo, porque no mostrará el Display de un error. Es decir, al escribir un programa CLI real, de hecho usaré el enfoque anterior, por lo que si quiero que mis ejemplos reflejen la realidad, creo que debería mostrar lo que hago en programas reales y no tomar atajos (en una medida razonable). ). De hecho, creo que este tipo de cosas son realmente importantes, y un efecto secundario de esto históricamente fue que mostró a la gente cómo escribir código Rust idiomático sin esparcir unwrap por todas partes. Pero si vuelvo a usar ?-in-main en mis ejemplos, entonces habré incumplido mi objetivo: ahora estoy configurando a personas que tal vez no saben nada mejor para escribir programas que, de forma predeterminada, emiten muy mensajes de error inútiles.

El patrón de "emitir un mensaje de error y salir con un código de error apropiado" se usa en programas pulidos. Por ejemplo, si ?-in-main usó Display , entonces podría fusionar las funciones main y run en ripgrep hoy:

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

Por supuesto, podría usar ?-in-main en el futuro proporcionando mi propio impl para el rasgo Termination una vez que se estabilice, pero ¿por qué me molestaría en hacer eso si pudiera escribir los main función que tengo? Y aun así, esto no ayuda con mi enigma al escribir ejemplos. Tendría que incluir ese impl en los ejemplos para que coincidieran con la realidad y, llegados a ese punto, también podría quedarme con los ejemplos que tengo hoy (usando un main y a try_main ).

Por lo que parece, parece que arreglar esto sería un cambio radical. Es decir, este código se compila hoy en Rust estable:

#[derive(Debug)]
struct OnlyDebug;

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

Creo que cambiar a Display rompería este código. ¡Pero no lo sé con seguridad! Si esto es realmente un problema de barco ha zarpado, entonces lo entiendo y no tiene mucho sentido insistir en el punto, pero me siento lo suficientemente fuerte como para al menos decir algo y ver si puedo convencer a otros y ver si hay algo que se pueda hacer para solucionarlo. (También es muy posible que esté exagerando aquí, pero hasta ahora un par de personas me han preguntado "¿por qué no estás usando ?-in-main ?" en mis ejemplos CSV, y mi respuesta básicamente ha sido, "No veo cómo sería factible hacer eso". Tal vez ese no era un problema que pretendía resolver ?-in-main , pero algunas personas ciertamente tenían esa impresión. Con su implementación actual , pude verlo útil en pruebas de documentos y pruebas unitarias, pero me cuesta pensar en otras situaciones en las que lo usaría).

Todos 183 comentarios

¿Cómo se van a tratar los estados de salida?

Este comentario de @Screwtapello parece haberse hecho demasiado cerca del final de FCP para que se realicen modificaciones en el RFC en respuesta a él.

En resumen: el RFC propone devolver 2 en caso de falla por motivos que, si bien están bien fundados, son oscuros y producen un resultado ligeramente inusual; lo menos sorprendente es devolver 1 cuando el programa no tiene ninguna indicación de que quiera más detalles que el éxito o el fracaso. ¿Es esto lo suficientemente complicado como para que se pueda discutir sin que parezca que estamos pervirtiendo el proceso de RFC, o ahora estamos atrapados en este detalle de implementación específico?

Sin embargo, no es un detalle de implementación, ¿verdad?

Algunos scripts usan códigos de salida como una forma de obtener información de un subproceso.

Se trata específicamente del caso en el que un subproceso (implementado en Rust) no tiene información para brindar, más allá de un binario "todo está bien"/"algo salió mal".

Algunos scripts usan códigos de salida como una forma de obtener información de un subproceso.

Ese comportamiento siempre depende en gran medida del programa que se llama _excepto_ en que un valor distinto de cero significa falla. Dado que std::process::exit con un contenedor de función principal y una tabla de búsqueda seguirá siendo la mejor opción para aquellos que desean un estado de salida más articulado sin importar lo que se haga, esto parece un detalle mayormente insignificante.

Sin embargo, no creo que SemVer tenga una excepción de "detalles en su mayoría insignificantes".

Creo que el código de salida debería agregarse a la lista de preguntas sin resolver. @zackw también abrió un hilo interno relacionado.

Muchas personas están de acuerdo en que el código de salida debe ser 1 en caso de falla (en lugar de 2 ):
https://www.reddit.com/r/rust/comments/6nxg6t/the_rfc_using_in_main_just_got_merged/

@ arielb1 ¿vas a implementar este rfc?

@bkchr

No, solo para guiarlo. Asigné para no olvidar escribir las notas de tutoría.

Ahh que bueno, me interesaría hacerlo :)
Pero no se por donde empezar :D

@bkchr

Es por eso que estoy aquí :-). Debería escribir las instrucciones de tutoría lo suficientemente pronto.

Bien, entonces estoy esperando sus instrucciones.

Instrucciones de tutoría

Este es un problema de [WG-compiler-middle]. Si desea buscar ayuda, puede unirse a #rustc en irc.mozilla.org (soy arielby) o https://gitter.im/rust-impl-period/WG-compiler-middle (soy @arielb1 allí).

Hay un archivo Léame del compilador WIP en #44505 - describe algunas cosas en el compilador.

Plan de trabajo para este RFC:

  • [ ] - agregue el elemento de idioma Termination a libcore
  • [ ] - permite usar Termination en main
  • [ ] - permite usar Termination en doctests
  • [ ] - permite usar Termination en #[test]

agregue el elemento de idioma Termination a libcore

Primero, debe agregar el rasgo Termination a libcore/ops/termination.rs , junto con cierta documentación. También deberá marcarlo como inestable con un atributo #[unstable(feature = "termination_trait", issue = "0")] ; esto evitará que las personas lo usen antes de que se estabilice.

Luego, debe marcarlo como un elemento de idioma en src/librustc/middle/lang_items.rs . Esto significa que el compilador puede encontrarlo al verificar el tipo de main (por ejemplo, vea 0c3ac648f85cca1e8dd89dfff727a422bc1897a6).
Eso significa:

  1. agregándolo a la lista de elementos de idioma (en librustc/middle/lang_items.rs )
  2. agregando un #[cfg_attr(not(stage0), lang = "termination")] al rasgo Termination . La razón por la que no puede simplemente agregar un atributo #[lang = "termination"] es porque el compilador "stage0" (durante el arranque) no sabrá que termination es algo que existe, por lo que no podrá compilar libstd. Eliminaremos manualmente los cfg_attr cuando actualicemos el compilador stage0.
    Consulte los documentos de arranque en XXX para obtener más información.

permitir usar Termination en main

Esta es la parte interesante con la que sé cómo lidiar. Esto significa hacer un main que devuelva () sin verificación de tipo (actualmente obtiene un error main function has wrong type ) y que funcione.

Para hacer una verificación de tipo, primero debe eliminar el error existente en:
https://github.com/rust-lang/rust/blob/9a00f3cc306f2f79bfbd54f1986d8ca7a74f6661/src/librustc_typeck/lib.rs#L171 -L218

Luego, debe agregar una verificación de que el tipo de retorno implementa el rasgo Termination (agrega una obligación de rasgo usando register_predicate_obligation - busque usos de eso). Eso se puede hacer aquí:
https://github.com/rust-lang/rust/blob/9a00f3cc306f2f79bfbd54f1986d8ca7a74f6661/src/librustc_typeck/check/mod.rs#L1100 -L1108

La otra parte es hacer que funcione. Eso debería ser bastante fácil. Como dice el RFC, desea hacer que lang_start sea genérico sobre el tipo de devolución.

lang_start se define actualmente aquí:
https://github.com/rust-lang/rust/blob/9a00f3cc306f2f79bfbd54f1986d8ca7a74f6661/src/libstd/rt.rs#L32

Por lo tanto, deberá cambiarlo para que sea genérico y coincida con el RFC:

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

    sys::init();

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

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

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

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

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

Y luego deberá llamarlo desde create_entry_fn . Actualmente, crea una instancia de un lang_start monomórfico usando Instance::mono , y deberá cambiarlo para usar monomorphize::resolve con los substs correctos.

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

permitir usar Termination en doctests

Realmente no entiendo cómo funcionan los doctests. ¿Quizás preguntarle a @alexcrichton (eso es lo que yo haría)?

permitir usar Termination en #[test]

Realmente no entiendo cómo funciona libtest. ¿Quizás preguntarle a @alexcrichton (eso es lo que yo haría)? Las pruebas unitarias son básicamente generadas por una macro, por lo que debe cambiar esa macro, o su llamador, para manejar los tipos de retorno que no son () .

@bkchr

¿Puedes al menos unirte al IRC/gitter?

@bkchr acaba de registrarse: vi que tú y @arielb1 estaban conversando sobre gitter hace algún tiempo, ¿algún progreso? ¿Conseguir chupar en alguna parte?

No lo siento, no hay progreso hasta ahora. Actualmente tengo muchas cosas que hacer, pero espero encontrar algo de tiempo esta semana para comenzar con esto.

@bkchr Si necesita ayuda, hágamelo saber.

Actualmente estoy un poco atascado, quiero crear la Obligación. Para crear la Obligación necesito un TraifRef, para un TraitRef necesito un DefId. ¿Puede alguien indicarme algún código sobre cómo crear un DefId a partir del rasgo de terminación?

Sí, ese no es el problema, ya lo hice. Necesito verificar el rasgo de terminación en la función check_fn. Quiero usar register_predicate_obligation y para eso necesito la definición del rasgo de terminación.

Oh, entonces todo lo que necesitas es tcx.require_lang_item(TerminationTraitLangItem) .

@bkchr ¿cómo va? Solo registrándome de nuevo. =) No te preocupes si estás ocupado, solo quiero asegurarme de que estás recibiendo toda la ayuda que necesitas.

Lo siento, estoy ocupado en este momento:/ Hasta ahora, recibí toda la ayuda que necesitaba :)

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

¿Creo que no verifico el tipo de devolución de la función? Obtuve el siguiente error:

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

¿Qué necesito cambiar para verificar el tipo de retorno de la función?

@bkchr Recomiendo unirse al grupo de trabajo del compilador intermedio gitter en https://gitter.im/rust-impl-period/WG-compiler-middle para recibir comentarios, así como probar el canal IRC #rust-internals en https ://chat.mibbit.com/?server=irc.mozilla.org%3A%2B6697&channel=%23rust-internals. :)

@bstrie , sí, gracias, ya formo parte del chat de gitter y podría resolver mi problema. :)

@bkchr su problema está en esta línea . La referencia de rasgo que desea generar allí es algo como R: Termination donde R es el tipo de retorno de la función. Esto se especifica mediante la creación de "sustancias" apropiadas, que es el conjunto de valores para sustituir los parámetros de tipo del rasgo (en este caso, Self ).

Sin embargo, está invocando el método Substs::identity_for_item en el rasgo. Esto le devolverá las sustituciones que uno usaría dentro de la propia definición de rasgo . es decir, en este caso está mapeando el parámetro Self declarado en el rasgo Termination a Self . Esto sería apropiado si estuviera verificando la definición de alguna función dentro del rasgo Terminator , pero no tanto aquí.

En cambio, lo que desea es obtener el tipo de retorno de la función de entrada. Esta es solo una de las variables ret_ty o actual_ret_ty . Cualquiera de los dos está bien, pero supongo que ret_ty es mejor, que corresponde al tipo de retorno que declaró el usuario (mientras que actual_ret_ty es el tipo que devolvió el código real).

Puede hacer las sustituciones que desee simplemente llamando al método mk_substs() desde el tcx. En este caso, solo hay un parámetro, el tipo, por lo que creo que algo como let substs = fcx.tcx.mk_substs(&[ret_ty]); funcionaría.

Creo que lo que hay que usar es tcx.mk_substs_trait(ret_ty, &[]) .

@bkchr acaba de registrarse: ¿tuvo la oportunidad de poner en práctica ese consejo? (Además, para obtener respuestas más rápidas, puede ser conveniente preguntar en gitter ).

Sí, podría resolver el problema con gitter :)

@bkchr ¿cómo va? Solo registrándome.

Todo bien, probablemente tendré algo de tiempo esta semana para revisar el código.

¿Hay lugar para que una persona más ayude con esto? Me gustaría comenzar a contribuir con la comunidad de Rust antes de fin de año y me encantaría ayudar con esta función. Con suerte, no sería demasiado confuso tener dos personas colaborando en esto.

@U007D

Esta es una característica pequeña y @bkchr casi ha terminado con ella.

Ah, ok, es bueno saberlo, gracias. Estaré atento a algo más en lo que pueda ayudar.

@lnicola ¡ Sí, lo tengo! Estoy tratando de encontrar algo en la intersección de algo en lo que me sienta seguro de poder trabajar (es decir, ser un positivo neto) y que me apasione. Para ser honesto, a pesar de que he estado aprendiendo Rust durante aproximadamente un año, todavía es un poco intimidante ofrecerse como voluntario para algo. FWIW, de ninguna manera es culpa de la comunidad de Rust: la comunidad de Rust se ha esforzado al máximo para hacer de esta una cultura abierta, acogedora e inclusiva, la mejor que he tenido el placer de experimentar. (Sospecho que tiene más que ver con viejas cicatrices de batalla de años y años de experiencia en la industria tecnológica, donde los equipos tienden a ser competitivos en lugar de colaborativos).

De todos modos, mi objetivo es elegir algo este año y al menos comenzar a hacer una contribución positiva. ¡Es hora de que me involucre! :)

Gracias por la sugerencia, @lnicola. Ese es un buen recurso.

@bkchr ¿ alguna actualización?

Estoy en eso (https://github.com/rust-lang/rust/pull/46479). Ahora tengo vacaciones y tiempo para trabajar en los comentarios de la solicitud de incorporación de cambios. Perdón por todos los retrasos :/

Oh, lo siento, no me di cuenta de que tenías una solicitud de extracción. Lo entrecruzó.

Hola, eh. Así que pensé en comenzar mi potencial carrera como colaborador de Rust haciendo un cobertizo de bicicletas, como es tradición. En concreto, sobre este:

  • [ ] El nombre del rasgo que se está introduciendo

¿Qué tal Exit ? Es breve y va al grano, y se ajusta al vocabulario existente de Rust. Exit-as-a-noun es una contraparte natural de exit-as-a-verb que, para la mayoría, es la palabra familiar para terminar un proceso "desde adentro" de manera controlada.

Para un programador de C++ específicamente, la "terminación" le recuerda a std::terminate que por defecto es una terminación anormal (llamando a abort ) y es básicamente el equivalente en C++ a un pánico (pero a diferencia de un pánico, nunca desenrolla el apilar).

Espere, ignore ese comentario, parece que el RFC dejó eso explícitamente abierto a discusión.

Me gusta Exit como nombre de rasgo.

Me imagino que la función se estabilizará mucho antes que el rasgo, como sucedió con Carrier .

FWIW, ese es otro caso en el que estoy muy contento de que el nombre provisional se haya cambiado antes de la estabilización: D

Como autor del RFC, no tengo ninguna objeción a cambiar el nombre del rasgo a Exit , o cualquier otra cosa realmente. No soy particularmente bueno para nombrar las cosas y estoy feliz de que alguien más tenga una mejor idea.

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

¿Se supone que el rasgo es

  1. colocado en libstd en lugar de libcore, y
  2. acaba de llamar std::Termination , no std::ops::Termination ?

El rasgo no se pudo colocar en libcore , porque la implementación de Result requiere imprimir en stderr y eso no se puede hacer en libcore .

@bkchr El hecho de que impl esté en libstd no significa que el rasgo deba estar también en libstd.

@kennytm Lo sé, pero Result también se define en libcore, por lo que Termination no se puede implementar para Result en libstd.

@zackw +1 voto más para Exit como el nombre del rasgo.

@U007D : ¿Podría usar el botón de reacciones (p. ej., 👍) en lugar de publicar ese mensaje? Eso le permitiría evitar problemas molestos a los suscriptores haciéndoles ping innecesariamente.

¿Puedo registrar libtest / libsyntax , si un language_feature está activado (en una caja)? @arielb1 @nikomatsakis @alexcrichton

@bkchr en libsyntax, es posible que deba pasarlo, pero en teoría es posible, pero en libtest en tiempo de ejecución, no creo que pueda verificarlo.

@bkchr ¿cómo va aquí?

Todavía estoy trabajando en ello, pero actualmente no tengo más preguntas :)

Creo que este impl es demasiado estricto:

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


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

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

Hay varios errores de uso común que no implementan Error , los más importantes son Box<::std::error::Error> y failure::Error . También creo que es un error usar el método description en lugar de la implementación de visualización de este error.

Propondría reemplazar este impl con este impl más amplio:

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

Eso pierde la cadena de causas, lo cual es un fastidio.

Sin embargo, usar la impl de visualización en lugar de la descripción es definitivamente lo mejor que se puede hacer.

La cadena de causas es un problema interesante. En particular, esta implementación solo imprime los dos primeros miembros de la cadena de causa.

las fallas tuvieron que lidiar con cómo manejar la cadena de causas y establecieron este comportamiento de forma predeterminada (por ejemplo, si solo genera errores usando .context :

  • {} imprime solo este error
  • {:?} imprime este error así como su causa (recursivamente)

Podríamos decidir usar :? aquí y vincularlo a Depurar en lugar de Mostrar. Inseguro.

Sí, ya sé que necesito mejorar el impl para dar soporte. Estoy abierto a lo que podríamos hacer. Vincular a Debug podría ser una buena idea.

Hmm, esto es complicado. Supongo que depende de si pensamos que un programa "pulido" hará uso de este rasgo impl. Tiendo a pensar que está bien decir que "no, no lo harán"; básicamente, un programa pulido (a) capturará la salida y la manejará de alguna otra manera o (b) usará algún tipo nuevo o algo que implemente Debug la direccion correcta. Esto significaría que podemos optimizar el impl para volcar información útil pero necesariamente en la forma más bonita (que parece ser el papel de Debug ).

Podría ser la elección correcta hacer que esto esté claramente dirigido a la creación de prototipos usando Debug , ya que no creo que podamos manejar automáticamente los errores de una manera que sea correcta para la mayoría de los casos de uso de producción.

@sinbarcos Estoy de acuerdo.

@nikomatsakis Supongo que quiso decir " no necesariamente en la forma más bonita". Si es así, sí, estoy de acuerdo.

Actualización: después de trabajar en esto durante un par de días, lo volteé. Vea abajo.

:+1: en Debug , aquí; Me gusta el "tipo de análogo a una excepción no detectada" de @nikomatsakis de https://github.com/rust-lang/rfcs/pull/1937#issuecomment -284509933. Un comentario de Diggsey que también sugiere Debug : https://github.com/rust-lang/rfcs/pull/1937#issuecomment -289248751

FYI, activé el problema predeterminado "más completo" frente a "más fácil de usar" (es decir, Debug frente a Display límite de rasgos).

El TL;DR es que ahora creo que deberíamos establecer el límite para estar en Display (según la publicación original de @withoutboats ) para proporcionar un resultado más limpio y resumido en el caso de 'no hacer nada'.

Aquí está mi razón:

En el problema de RFC del rasgo termination , @zackw destaca que Rust tiene el sistema dual panic / Result porque panic son para errores y Result son para errores. A partir de esto, creo que se puede hacer un caso convincente para evaluar la presentación de error por defecto independientemente de la presentación de pánico por defecto.

Por supuesto, no existe un valor predeterminado que satisfaga a todos, por lo que, aplicando el principio de la menor sorpresa, me pregunto qué valor predeterminado es más apropiado.

  • Un error a menudo no es manejado por el diseño , ya que está destinado a ser comunicado al usuario que algo (posiblemente reparable) salió mal (archivo no encontrado, etc.). Como tal, existe el caso de uso y podría considerarse razonablemente común que el usuario es la audiencia prevista.

  • Como señaló @nikomatsakis , independientemente del valor predeterminado que elijamos, cualquier desarrollador que desee cambiar el comportamiento puede usar el patrón newtype o desarrollar una implementación personalizada en main().

Y, por último, en el lado más subjetivo, al trabajar con esta función durante los últimos días, descubrí que el resultado Debug me dejó con la sensación de que mi "aplicación Rust" se sentía menos pulida :

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

contra

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

El rasgo Dispay parece ser un valor predeterminado mucho más civilizado para un error (a diferencia de un error), ¿no es así?

@U007D espera, ¿cuál de esas dos salidas prefieres?

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

o

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

Prefiere la opción (b).

@nikomatsakis Originalmente, estaba bien con a) Debug como concepto en mi cabeza, pero después de trabajar con él durante un par de días para ver el resultado, ahora prefiero b) Display como un valor predeterminado Creo que mi preferencia por b) sería aún más pronunciada si estuviera modelando un error encadenado.

Sin embargo, no creo que "pulido" o "civilizado" sea el objetivo de esto, ya que entendí que el hilo ya había aceptado esto como principalmente para ejemplos, y se espera que las personas agreguen un manejo personalizado a medida que el programa madura.

En esos casos, para mí, la "menor sorpresa" es una salida orientada al desarrollador como unwrap .

¿Valdría la pena discutir {:#?} aquí, si existe la preocupación de un error largo?

El informe de errores para los usuarios finales será diferente para cada herramienta y cada caso de uso, pero el informe de errores para los desarrolladores debería parecerse a lo que hace Rust en otras situaciones como .unwrap() . Dado que solo puede haber un valor predeterminado, y el software pulido deberá anular la salida de todos modos, voto por que el valor predeterminado esté orientado al desarrollador con Debug .

Creo que el corazón de esta discusión es realmente "¿quién es el público objetivo del mensaje predeterminado?"

Digamos por un momento que todos estuvimos de acuerdo en que el público objetivo predeterminado eran los desarrolladores. Creo que el límite predeterminado Debug sería una opción sencilla.

Ahora digamos por un momento que acordamos que el público objetivo predeterminado era el usuario, entonces aquí es donde, respetuosamente, no estoy de acuerdo con algunos otros y siento que las cualidades subjetivas como la salida "pulida" y "civilizada" tienen una parte importante para jugar. Para algunos, "pulir" la presentación del usuario final puede ser la mejor razón de todas para evitar Display . (No comparto esa opinión, pero la entiendo y la respeto).

Así que sí, ciertamente puedo ver argumentos razonables para cualquiera de los dos grupos como objetivo predeterminado. Creo que si se desarrolla un fuerte consenso sobre qué audiencia debe ser el objetivo predeterminado, entonces la elección del límite de características será clara (er)... :)

(No estoy completamente versado en todo este tema, pero) ¿no es concebible que pueda haber pequeñas utilidades para las cuales la salida de error predeterminada con Termination sería perfectamente adecuada, siempre que esté en algún formato presentable para el usuario? como Display ? En ese caso, la única razón por la que los autores tendrían que buscar el "manejo personalizado" es si los hacemos.

¿Alguien puede proporcionar ejemplos de cómo se ve la salida en cada caso (supongo que también depende del tipo particular E utilizado?) y qué pasos deben seguir los autores si quieren un "manejo personalizado" ¿en lugar de? Solo voy a las hipótesis anteriores.

(¿La salida se parece literalmente a lo que @U007D pegó arriba? ¿Por qué imprimiría "Caja devuelta\Caja<Error>?)

¿Con qué frecuencia incluso el Display del mensaje de error es lo suficientemente fácil de usar? Por ejemplo, el siguiente programa:

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

emite el siguiente mensaje:

No such file or directory (os error 2)

Lo cual, diría, no es una gran experiencia de usuario, especialmente si no se menciona el nombre del archivo. Al menos no a menos que el programa literalmente tome un solo nombre de archivo como entrada. Por otro lado, tampoco es una gran experiencia para el desarrollador , ya que falta el archivo fuente/número de línea/rastreo de pila. El resultado Debug es, obviamente, una experiencia de usuario aún peor, y tampoco agrega información útil para el desarrollador.

Así que supongo que lo que estoy tratando de decir es que sin mejorar el contenido de información de los propios errores de la biblioteca, ni Debug ni Display son geniales.

¿La salida se parece literalmente a lo que @U007D pegó arriba? ¿Por qué imprimiría "Caja devueltafrom main()" en lugar de... el contenido real de ese Box?

@glaebhoerl Tienes razón: en este caso, el "contenido real de ese Box<Error> " era un campo message personalizado que había creado para probar el termination_trait , que se muestra palabra por palabra . Podría haber escrito "foo bar baz" o cualquier otra cosa allí (pero eso podría no haber sido tan útil para un usuario que ejecuta las pruebas del compilador).

Usando el ejemplo de @jdahlstrom , aquí está la salida real para una biblioteca estándar Box ed "archivo no encontrado" Error (tenga en cuenta que, como usted señala correctamente, no se menciona el boxeo en ninguna parte):
Debug :

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

y Display :

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

@jdahlstrom Creo que tienes un buen punto. Estoy de acuerdo en que, si bien ambos mensajes pueden desatender a su público objetivo, quiero resaltar que proporcionar el mensaje incorrecto es aún peor (como creo que aludió):

Proporcionar Display a un desarrollador tiene todas las desventajas de Debug además de perder la especificidad de qué tipo de error se muestra.

Proporcionar Debug a un usuario tiene todas las desventajas de Display y además agrega aún más información técnica que el usuario no necesita y es posible que no pueda entender.

Entonces, sí, estoy de acuerdo en que los mensajes a menudo no están lo suficientemente dirigidos a ninguna de las audiencias. Creo que esto resalta otra razón importante para que tengamos claro a quién nos dirigimos para brindar la mejor experiencia posible (a pesar de las fallas) para ese grupo.

Necesito ayuda para implementar el soporte para ? en #[test] . Mi implementación actual se puede encontrar aquí: https://github.com/rust-lang/rust/compare/master...bkchr :termination_trait_in_tests

Compilar una prueba con mis cambios da como resultado el siguiente error:

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

@eddyb dijo que ya no debería usar quote_item!/expr! , porque son heredados.
¿Qué debo hacer ahora, cambiar a la nueva macro quote! o volver a trabajar en la construcción manual de ast?

Agradezco cualquier ayuda :)

Creo que generar una invocación de macro a alguna macro definida en libtest podría funcionar muy bien.

@eddyb No estoy seguro de entender su sugerencia aquí:

Creo que generar una invocación de macro a alguna macro definida en libtest podría funcionar muy bien.

Oh, supongo que tal vez sí. ¿Está diciendo: definir una macro en libtest y luego generar un código que la invoque? Idea interesante. Sin embargo, ¿no se "filtraría" ese nombre de macro? (es decir, ¿se convierte en parte de la interfaz pública de libtest?)


@bkchr

Compilar una prueba con mis cambios da como resultado el siguiente error:

¿Tienes alguna idea de por qué se genera ese error? Solo al leer la diferencia, no lo hago, pero puedo intentar construir localmente y resolverlo.

Ya no debería usar quote_item!/expr! , porque son heredados.

No tengo una opinión fuerte aquí. Estoy de acuerdo con @eddyb , son heredados, pero no estoy seguro de si son el tipo de legado en el que agregar algunos usos más los hará más difíciles de eliminar, es decir, una vez que obtengamos un reemplazo reciente, ¿sería fácil? @eddyb para pasar de uno a otro?

La construcción manual de AST es ciertamente una molestia, aunque supongo que tenemos algunos ayudantes para eso.

Principalmente solo necesitamos hacer una pequeña edición, ¿verdad? es decir, ¿cambiar de invocar la función a probar el resultado de report() ?

PD: probablemente queramos generar algo como Termination::report(...) en lugar de usar una notación .report() , para evitar depender de que el rasgo Termination esté dentro del alcance.

No, no tengo ni idea de dónde viene ese error :(

El RFC propuso generar una función contenedora que llame a la función de prueba original. Esa es también mi forma actual de hacerlo.
Creo que también podríamos descartar la función contenedora, pero luego requeriríamos encajonar el puntero de función, ya que cada función de prueba puede devolver un tipo diferente.

Hmm, como la prueba ya importa otras cosas, no es tan complicado importar también el rasgo de Terminación.

@alexcrichton , ¿quizás tenga una idea de dónde proviene este error?

@nikomatsakis libtest es inestable y ¿no podemos marcar las macros como inestables incluso si no lo fueran?

@eddyb oh, buen punto.

El RFC propuso generar una función contenedora que llame a la función de prueba original. Esa es también mi forma actual de hacerlo.

Una función de contenedor me parece bien.

@eddyb con la macro, ¿te refieres a algo como create_test que se define en libtest ? Pero lo que no entiendo es "marcar macro como inestable". ¿Qué pretendes con eso? ¿Podrías darme un ejemplo?

@bkchr Poner un atributo #[unstable(...)] en la definición de macro, por ejemplo: https://github.com/rust-lang/rust/blob/3a39b2aa5a68dd07aacab2106db3927f666a485a/src/libstd/thread/local.rs#L159 -L165

Entonces, si esta primera casilla de verificación...

Implementar el RFC

...ser verificado ahora que el PR vinculado se ha fusionado?

@ErichDonGubler Listo :)

Hmm, actualmente solo se implementa la mitad de rfc con el pr combinado ^^

Separado en 3 casillas de verificación :)

He estado tratando de usar esta función en main y me resulta bastante frustrante. Los impls existentes del rasgo de terminación simplemente no me permiten "acumular" convenientemente múltiples tipos de errores; por ejemplo, no puedo usar failure::Fail , porque no implementa Error ; No puedo usar Box<Error> , por la misma razón. Creo que deberíamos priorizar el cambio a Debug . =)

Hola, @nikomatsakis ,

Sentí exactamente la misma frustración que tú cuando intenté usar termination_trait .

Eso, combinado con sus publicaciones sobre la piratería en el compilador, me inspiró a resolver este problema a principios de este mes. He publicado el impl por Display (y por Debug en la confirmación anterior) junto con las pruebas aquí: https://github.com/rust-lang/rust/pull/47544. (Es muy pequeño, pero aún así, ¡mi primer compilador de Rust PR! :tada:) :)

Tengo entendido que el equipo de lang hará una llamada sobre qué rasgo elegir, pero de cualquier manera, la implementación está lista para comenzar.

Una pregunta que todavía me interesa: supongamos que no quiere confiar en la salida del mensaje de error predeterminado (ya sea Debug o Display ), y quiere la suya propia, ¿cómo lo hace? ¿que? (Disculpas si esto ya estaba escrito en alguna parte y me lo perdí). No tendrías que dejar de usar ? -en- main por completo, ¿verdad? Es algo así como escribir su propio resultado y/o tipo de error y/o impl ? (Me parece desafortunado si ? -en- main fueran solo un juguete, y tan pronto como quisieras "ponerte serio" tendrías que volver a formas menos ergonómicas).

@glaebhoerl Es muy sencillo:

  1. Crear un nuevo tipo.
  2. Implemente From su antiguo tipo de error.
  3. Implementar Debug (o Display ).
  4. Reemplace el tipo en la firma de main .

¡Gracias!

Me parece un poco extraño escribir implementaciones personalizadas de Debug que no están orientadas a la depuración, pero supongo que no es el fin del mundo.

@glaebhoerl Es por eso que Result impl debería usar Display en lugar de Debug IMO.

¿No puede el rasgo Termination tener un método extra llamado message , o error_message o algo así, que tiene una implementación predeterminada que usa Debug / Display para mostrar el mensaje? Luego, solo tiene que implementar un solo método en lugar de crear un nuevo tipo.

@glaebhoerl @Thomasdezeeuw Algo así como un método error_message estaba en el borrador original del RFC, pero se eliminó por falta de consenso. Mi sensación en ese momento era que sería mejor obtener la característica básica aterrizada (no necesariamente estabilizada) y luego iterar.

@zackw De acuerdo, personalmente estaría bien sin mensajes, solo un número, digamos 0 y 1 para códigos de error. Pero si queremos obtener el mensaje en la primera iteración, creo que estaría más a favor de un Termination::message que cualquier cosa en Debug o Display .

¿Ese método message devolvería un String ? ¿No sería eso incompatible con Termination en libcore?

@SimonSapin Termination está definido actualmente en libstd .

Hmm, pero no creo que agregar un método message al rasgo sea la mejor implementación. ¿Qué devolvería el método para tipos como i32 ? ¿Cómo decidiría cuándo imprimir este mensaje? Actualmente la implementación de Termination para Result , imprime el error en la función report . Eso funciona, porque Result sabe que es un Err . Podríamos integrar en algún lugar un cheque report() != 0 y luego imprimirlo, pero eso no se siente bien.
El siguiente problema sería que queremos proporcionar una implementación estándar para Result , pero ¿qué necesita implementar el tipo Error para que se imprima probablemente? Esto nos llevaría de vuelta a la pregunta actual Debug o Display .

Otro dolor de cabeza que surge si desea usar ? en main en un programa "serio" es que, en algunas circunstancias, los programas de línea de comandos quieren salir con un estado distinto de cero pero sin imprimir _nada_ (considere grep -q ). Así que ahora necesita un Termination impl para algo que _no_ es un Error , que no imprime nada, que le permite controlar el estado de salida... y debe decidir si desea o no volver a devolver esa cosa _después_ de analizar los argumentos de la línea de comando.

Esto es lo que pienso:

Devolver Result<T, E> debería usar el impl de depuración para E. Este es el rasgo más conveniente: está ampliamente implementado y satisface el caso de uso de "salida rápida y sucia", así como el caso de uso de prueba unitaria. Preferiría no usar Display porque está menos implementado y porque da la impresión de que se trata de un resultado pulido, lo que creo que es muy poco probable.

Pero también debería haber una manera de obtener resultados de aspecto profesional. Afortunadamente, ya hay dos formas de este tipo. Primero, como dijo @withoutboats , las personas pueden hacer un puente de "visualización desde la depuración" si quieren usar E: Display o dar un resultado de estilo profesional. Pero si esto parece demasiado complicado, también puede definir su propio tipo para reemplazar el resultado. por ejemplo, podrías hacer:

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

y luego implemente Try para ProfessionalLookingResult . Entonces puedes implementar Terminate también, lo que sea:

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

Estoy de acuerdo con @nikomatsakis en que esto debería usar Debug .

También creo que para una salida pulida, escribir algo de código en main probablemente sea mejor que crear un nuevo tipo para implementar Try y Terminate . Me parece que el punto de Terminate es para las cosas para las que las bibliotecas pueden fácilmente hacer un buen valor predeterminado, lo que nunca es el caso para una situación en la que la forma en que termina el programa es importante para los usuarios finales (por ejemplo, CLI profesionales) .

Por supuesto, otras personas pueden tener una opinión diferente, y hay múltiples formas de usar los rasgos involucrados para inyectar código en lugar de escribirlo directamente en main . Lo bueno es que tenemos múltiples opciones y no tenemos que pensar en una sola forma bendita de manejar los errores siempre.

Permítanme anotar algunos pensamientos, aunque hay algunos problemas con ellos.

Me encantaría ver lo siguiente

fn main() -> i32 {
    1
}

Que podría escribirse de manera más general como:

fn main() -> impl Display {
    1
}

Ambas funciones principales deberían devolver un código de salida 0 y println! el Display de 1.

Esto debería ser tan simple como lo siguiente (creo).

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

Entonces por errores podemos tener:

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

donde la implementación es la misma que en el RFC solo con "{:?}" para usar
un formato Debug .

Como se mencionó antes, las personas que necesitan más control sobre la salida pueden simplemente
escribe:

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

Aunque esto sería indecidible con nuestro compilador actual, supongo, ya que
vería implementaciones conflictivas... Así que hacemos lo que sugiere @nikomatsakis
y escribe:

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

Sé que esto es en parte reafirmar cosas que se han dicho, pero pensé en presentar mi visión en conjunto, mostrando que una solución más general para mostrar valores de retorno, no solo resultados. Parece que mucho de este comentario está discutiendo qué implementaciones de Termination enviamos por defecto. Además, este comentario está en desacuerdo con una implementación como impl Termination for bool como se describe en el RFC. Personalmente, creo que los códigos de salida distintos de cero deben ser manejados exclusivamente por Results o tipos personalizados que implementen Termination .

Es una pregunta interesante cómo manejar ? en tipos Option en main, ya que no tienen una implementación para Display .

TL;DR: Estoy bien con Debug .

Explicación más detallada:
Pasé algún tiempo pensando en esto ayer y hoy con el objetivo de descubrir suposiciones implícitas que tengo o estoy haciendo para ayudar a llegar a la mejor respuesta posible.

Suposición clave: de todos los diversos tipos de aplicaciones que se pueden escribir en Rust, creo que la aplicación de la consola es la que más se beneficiará o se verá más afectada por esta decisión. Mi opinión es que al escribir una biblioteca, un título de juego AAA, un IDE o un sistema de control patentado, uno probablemente no esperaría que un rasgo de terminación principal predeterminado satisfaga las necesidades de uno (de hecho, es posible que ni siquiera tenga un principal). Así que mi preferencia por Display como valor predeterminado proviene de lo que esperamos ver cuando usamos una pequeña aplicación de línea de comandos, por ejemplo:

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

La mayoría de nosotros no esperamos ningún tipo de ayuda de depuración, solo un indicador sucinto de lo que salió mal. Simplemente estoy abogando por esto como una posición predeterminada.

Cuando pienso en escribir un Terminate impl para obtener un resultado simple como este, me doy cuenta de que el ? de la función principal no es tan diferente del óxido estable actual (en términos de cantidad de código escrito ), donde a menudo se crea un "inner_main()" consciente Result para manejar E .

Con esta suposición en mente, como un ejercicio de pensamiento, traté de determinar si sentía firmemente que la preponderancia de las implementaciones de estilo " inner_main() " que existen hoy en día tenían un sabor Display más informal. (sobre un sabor más técnico de Debug ). Pensé que esto sería una indicación de cómo es probable que se use realmente la función.

No pude convencerme de que este fuera el caso. (Es decir, no creo que actualmente exista un fuerte sesgo hacia Display en las implementaciones existentes). De hecho, al revisar mis propios repositorios que he escrito durante los últimos 16 meses, también encontré suficientes de ambos casos que no puedo decir que implementar Display de manera predeterminada hubiera significado un ahorro neto.

Manteniendo la suposición de que "el principal beneficiario es la aplicación cli", hay una gran cantidad de aplicaciones de consola que brindan ayuda e información de uso. Por ejemplo:

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

The most similar command is
    log

Entonces, incluso en aplicaciones de consola, es difícil para mí identificar un "grupo lesionado" con Debug .

Y, por último, estaría más feliz con un impl de Debug que con la función retenida durante otros 6 meses también, así que, egoístamente, ahí está :).

Así que ahí está mi proceso de pensamiento expuesto en público. Para resumir, creo que Debug no será mejor ni peor que Display y, como tal, debería estar bien como implementación predeterminada.

Como muchos de ustedes, estoy seguro, desearía que hubiera una implementación que me entusiasmara más, como "¡¡SÍ, ES ESO!!!", TBH. Pero tal vez solo sean mis expectativas poco realistas... Tal vez una vez que tengamos una solución que funcione con failure reduciendo el modelo en mis proyectos, crecerá en mí. :)

Tenga en cuenta que abrí un PR para admitir el rasgo de terminación en las pruebas: # 48143 (basado en el trabajo de @bkchr ).

Me tomé la libertad de extender el rasgo Termination con un método para procesar el resultado de la prueba. Esto simplificó la implementación, pero también tiene sentido, ya que las fallas en las pruebas pueden requerir una salida más detallada que las fallas en los ejecutables.

Termination debe renombrarse Terminate después de nuestra preferencia general de verbos para rasgos en libstd.

@withboats Creo que en algún momento hubo una discusión de que los rasgos de los verbos son en su mayoría aquellos que tienen un solo método con el mismo nombre que el rasgo. De todos modos, ¿podría flotar nuevamente mi propia sugerencia de cobertizo para bicicletas, Exit ?

Bikeshedding gratuito: este es un rasgo de método único. Si queremos darles el mismo nombre, tal vez ToExitCode / to_exit_code ?

La estabilización de la devolución Result se puede hacer independientemente de la estabilización del rasgo, ¿verdad?

Para mí, esto se parece mucho a ? donde la mayor parte del valor proviene de la característica del idioma, y ​​podemos demorarnos en descubrir la característica. La discusión de RFC incluso me hizo preguntarme si el rasgo _alguna vez_ necesita ser estabilizado, ya que poner el código en un inner_main parecía más fácil que un rasgo impl para esto...

Sí, no necesitamos estabilizar el rasgo, aunque es útil en estable para cosas como marcos, que no necesariamente pueden depender tanto de inner_main .

@SimonSapin Creo que To se refiere a una conversión de tipo, y esto no. Pero podríamos nombrar el método terminate (tampoco creo que esta restricción sobre cuándo nombrar verbos de rasgos se mantenga. Try es un contraejemplo obvio).

He propuesto que estabilicemos fn main() -> T donde T no es la unidad. Esto deja muchos detalles, particularmente el nombre/ubicación/detalles del rasgo, sin estabilizar, pero algunas cosas están arregladas. Detalles aquí:

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

¡Por favor dé su opinión!

terminate parece ser más descriptivo que report . Siempre terminate , pero podemos omitir report ing.

Pero a diferencia de, por ejemplo std::process::exit este método no finaliza nada. Solo convierte el valor de retorno de main() en un código de salida (después de imprimir opcionalmente Result::Err en stderr).

Otro voto por Exit . Me gusta que sea breve, bastante descriptivo y consistente con el concepto tradicional de códigos de salida/estado de salida.

Definitivamente preferiría Exit a Terminate ya que saldremos con gracia al regresar de la página principal, en lugar de terminar abruptamente donde estamos porque algo salió realmente mal.

Agregué https://github.com/rust-lang/rust/issues/48854 para proponer pruebas unitarias estabilizadoras que arrojan resultados.

Oye, encontré el lugar adecuado para hablar de esto.

Usando ? en doctests

La forma en que funcionan actualmente las pruebas documentales es algo como esto:

  • rustdoc escanea el doctest para ver si declara un fn main

    • (actualmente solo hace una búsqueda de texto línea por línea para "fn main" que no está después de // )

  • Si se encontró fn main , no tocará lo que ya está allí
  • Si no se encontró fn main , envolverá la mayoría* de la prueba en un fn main() { } básico

    • *Versión completa: extraerá las declaraciones #![inner_attributes] y extern crate y las colocará fuera de la función principal generada, pero todo lo demás va dentro.

  • Si el doctest no incluye ninguna declaración de caja externa (y la caja que se está documentando no se llama std ), entonces rustdoc también insertará una declaración extern crate my_crate; justo antes de la función principal generada.
  • rustdoc luego compila y ejecuta el resultado final como un binario independiente, como parte del arnés de prueba.

(Omití un par de detalles, pero convenientemente hice una reseña completa aquí).

Entonces, para usar ? sin problemas en una prueba de documento, lo que debe cambiar es la parte donde agrega fn main() { your_code_here(); } Declarar su propio fn main() -> Result<(), Error> funcionará tan pronto como pueda hacerlo eso en el código normal: rustdoc ni siquiera necesita modificarse allí. Sin embargo, para que funcione sin declarar main de forma manual, se requerirá un pequeño ajuste. Al no haber seguido de cerca esta característica, no estoy seguro de si existe una solución única para todos. ¿Es posible fn main() -> impl Termination ?

¿Es fn main() -> impl Terminación posible?

En un sentido superficial, sí: https://play.rust-lang.org/?gist=8e353379f77a546d152c9113414a88f7&version=nightly

Desafortunadamente, creo que -> impl Trait es fundamentalmente problemático con ? debido a la conversión de error incorporada, que necesita el contexto de inferencia para decirle qué tipo usar: https://play.rust- lang.org/?gist=23410fa4fa684710bc75e16f0714ec4b&version=todas las noches

Personalmente, me estaba imaginando ? -in-doctests trabajando a través de algo como https://github.com/rust-lang/rfcs/pull/2107 como fn main() -> Result<(), Box<Debug>> catch { your_code_here(); } (usando la sintaxis de https:// github.com/rust-lang/rust/issues/41414#issuecomment-373985777).

Sin embargo, la versión impl Trait es genial y podría funcionar si hubiera algún tipo de soporte "preferir el mismo tipo si no está restringido" que rustc podría usar internamente en el desugar ? , pero mi recuerdo es que la idea de una función como esa tiende a hacer que las personas que entienden cómo funcionan las cosas retrocedan con horror :sweat_smile: Pero tal vez sería factible que sea algo interno que solo se aplica si la salida es impl Trait ...

Ooh, esas son preocupaciones muy reales. Me había olvidado de cómo eso estropearía la inferencia de tipos. Si el bloque catch comenzó a envolverse bien como ese problema vinculado, entonces parece un camino mucho más fácil (y mucho más rápido, en cuanto a la estabilización).

Lo único que me pregunto es cómo se verá afectado por la transición de la edición. ¿No está cambiando la sintaxis catch en la época de 2018? Rustdoc probablemente querrá compilar doctests en la misma edición que la biblioteca que está ejecutando, por lo que debería diferenciar entre las sintaxis según la marca de época que se le entregó.

Me preocupa que esto ahora esté estabilizado, pero los casos simples aparentemente siguen siendo ICE: https://github.com/rust-lang/rust/issues/48890#issuecomment -375952342

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

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

¿En qué momento decimos "esto tiene demasiados errores y debemos desestabilizarnos"? Parece que si esto llegara al canal estable en su forma actual, causaría mucha confusión.

@frewsxcv Creo que los problemas ya están solucionados, ¿verdad?

@nikomatsakis el problema que planteé en https://github.com/rust-lang/rust/issues/48389 se resolvió en 1.26-beta, así que sí desde mi perspectiva.

¡Sí, el ICE que me preocupaba ya está arreglado!

Disculpas por intervenir en un punto en el que probablemente sea demasiado tarde para hacer algo al respecto, pero quería dejar mis comentarios aquí en caso de que los hubiera. Leí la mayor parte de este hilo, así que hablo con ese contexto en mente. Sin embargo, este hilo es largo, por lo que si parece que he pasado por alto algo, entonces probablemente lo hice y agradecería que me lo indicaran. :-)

TL; DR: creo que mostrar el mensaje de error Debug fue un error, y que una mejor opción sería usar el mensaje de error Display .

En el centro de mi creencia es que, como alguien que _rutinariamente crea programas CLI en Rust_, no recuerdo haberme preocupado mucho por cuál es el mensaje Debug de un Error . Es decir, el Debug de un error es, por diseño, para los desarrolladores, no para los usuarios finales. Cuando crea un programa CLI, su interfaz está fundamentalmente destinada a ser leída por los usuarios finales, por lo que un mensaje Debug tiene muy poca utilidad aquí. Es decir, si algún programa CLI que envié a los usuarios finales mostrara la representación de depuración de un valor de Rust en funcionamiento normal, entonces consideraría que se ha corregido un error. En general, creo que esto debería ser cierto para todos los programas CLI escritos en Rust, aunque entiendo que puede ser un punto en el que las personas razonables pueden estar en desacuerdo. Dicho esto, una implicación un tanto sorprendente de mi opinión es que hemos estabilizado efectivamente una función en la que su modo de operación predeterminado lo inicia con un error (nuevamente, en mi opinión) que debe corregirse.

Al mostrar la representación Debug de un error de forma predeterminada, también creo que estamos fomentando una mala práctica. En particular, es muy común en el curso de escribir un programa Rust CLI observar que incluso el Display impl de un error no es lo suficientemente bueno para ser consumido por los usuarios finales, y que se debe hacer un trabajo real para arreglalo. Un ejemplo concreto de esto es io::Error . Mostrar un io::Error sin una ruta de archivo correspondiente (asumiendo que proviene de leer/escribir/abrir/crear un archivo) es básicamente un error, porque es difícil para un usuario final hacer algo con él. Al elegir mostrar la representación Debug de un error de forma predeterminada, hemos hecho más difícil que la gente que crea programas CLI descubra ese tipo de errores. (Además de eso, el Debug de un io::Error es mucho menos útil que su Display , pero eso por sí solo no es un gran problema en mi experiencia. )

Finalmente, para completar mi argumento, también me cuesta imaginar las circunstancias bajo las cuales usaría ?-in-main incluso en ejemplos. Es decir, he estado tratando de escribir ejemplos que coincidan lo más posible con programas del mundo real, y esto generalmente ha implicado escribir cosas como esta:

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

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

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

A primera vista, sería _encantador_ reemplazar esto con ?-in-main , pero no puedo, porque no mostrará el Display de un error. Es decir, al escribir un programa CLI real, de hecho usaré el enfoque anterior, por lo que si quiero que mis ejemplos reflejen la realidad, creo que debería mostrar lo que hago en programas reales y no tomar atajos (en una medida razonable). ). De hecho, creo que este tipo de cosas son realmente importantes, y un efecto secundario de esto históricamente fue que mostró a la gente cómo escribir código Rust idiomático sin esparcir unwrap por todas partes. Pero si vuelvo a usar ?-in-main en mis ejemplos, entonces habré incumplido mi objetivo: ahora estoy configurando a personas que tal vez no saben nada mejor para escribir programas que, de forma predeterminada, emiten muy mensajes de error inútiles.

El patrón de "emitir un mensaje de error y salir con un código de error apropiado" se usa en programas pulidos. Por ejemplo, si ?-in-main usó Display , entonces podría fusionar las funciones main y run en ripgrep hoy:

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

Por supuesto, podría usar ?-in-main en el futuro proporcionando mi propio impl para el rasgo Termination una vez que se estabilice, pero ¿por qué me molestaría en hacer eso si pudiera escribir los main función que tengo? Y aun así, esto no ayuda con mi enigma al escribir ejemplos. Tendría que incluir ese impl en los ejemplos para que coincidieran con la realidad y, llegados a ese punto, también podría quedarme con los ejemplos que tengo hoy (usando un main y a try_main ).

Por lo que parece, parece que arreglar esto sería un cambio radical. Es decir, este código se compila hoy en Rust estable:

#[derive(Debug)]
struct OnlyDebug;

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

Creo que cambiar a Display rompería este código. ¡Pero no lo sé con seguridad! Si esto es realmente un problema de barco ha zarpado, entonces lo entiendo y no tiene mucho sentido insistir en el punto, pero me siento lo suficientemente fuerte como para al menos decir algo y ver si puedo convencer a otros y ver si hay algo que se pueda hacer para solucionarlo. (También es muy posible que esté exagerando aquí, pero hasta ahora un par de personas me han preguntado "¿por qué no estás usando ?-in-main ?" en mis ejemplos CSV, y mi respuesta básicamente ha sido, "No veo cómo sería factible hacer eso". Tal vez ese no era un problema que pretendía resolver ?-in-main , pero algunas personas ciertamente tenían esa impresión. Con su implementación actual , pude verlo útil en pruebas de documentos y pruebas unitarias, pero me cuesta pensar en otras situaciones en las que lo usaría).

Acepto que mostrar Display sobre Debug es mejor para las aplicaciones CLI. No estoy de acuerdo con que deba ser Display en lugar de Debug , porque eso limita drásticamente los errores que en realidad pueden ser ? y anula el propósito de ?-in-main . Por lo que puedo decir (podría estar totalmente equivocado ya que no tengo tiempo para compilar stdlib y no sé si la especialización cubre esto) no hay ninguna razón por la que no podamos agregar el siguiente impl a Terminación. Esto proporcionaría una forma ininterrumpida de usar Display cuando esté disponible y recurrir a Debug cuando no lo esté.

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

Finalmente, para completar mi argumento, también me cuesta imaginar las circunstancias bajo las cuales usaría ?-in-main incluso en ejemplos.

Tengo que estar de acuerdo con @BurntSushi en general para los programas CLI reales, pero para los scripts aleatorios y las herramientas internas que solo planeo usar, esto es realmente conveniente. Además, es muy conveniente para prototipos y juguetes. Siempre podríamos desalentar su uso en el código de producción, ¿verdad?

Parte del objetivo con ?-in-main es evitar el código no idiomático en los ejemplos de código, es decir, los desenvolvimientos. Pero si ?-in-main en sí mismo se vuelve no idiomático, eso socava toda la característica. Me gustaría encarecidamente evitar cualquier resultado que haga que tengamos que desalentar su uso, en el código de producción o de otra manera.

He estado tratando de usar esta función en main y me ha resultado bastante frustrante. Los impls existentes del rasgo de terminación simplemente no me permiten "acumular" convenientemente múltiples tipos de errores; por ejemplo, no puedo usar fail::Fail, porque no implementa Error; no puedo usar caja, Misma razón. Creo que deberíamos priorizar el cambio a Debug. =)

Si usamos Display , podemos introducir un tipo rápido y sucio como MainError para acumular varios tipos de errores:

pub struct MainError {
    s: String,
}

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

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

Esto permitirá algo similar a Box<Error> a continuación:

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

La discusión anterior de Display vs Debug está en la parte oculta aquí, comenzando alrededor de https://github.com/rust-lang/rust/issues/43301#issuecomment -362020946.

@BurntSushi Estoy de acuerdo contigo. Si esto no se puede arreglar, podría haber una solución alternativa:

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

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

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

Si esto estuviera en std::fmt , podríamos usar algo como esto:

use std::{fmt, io};

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

Todavía no tengo tiempo para contribuir más a esto en el futuro previsible, pero también estoy de acuerdo en que ? en main debería poder usarse en programas CLI serios, y había características en el borrador original del RFC que estaban destinados a facilitar eso. Se eliminaron en nombre del incrementalismo, pero tal vez deberían revisarse.

Creo que está bien arreglar esto de una manera incompatible con versiones anteriores si se hace pronto, antes de que mucho código lo esté usando en estable.

Como alguien que escribe muchos programas CLI en Rust y que está a cargo del estilo de Rust en el trabajo, estoy bastante de acuerdo con @burntsushi aquí. Habría usado felizmente la versión de esta característica especificada en el RFC.

Pero considero que mostrar una implementación de Debug a un usuario es un error, y no mucho mejor que simplemente llamar a unwrap en todas partes en un programa CLI. Entonces, incluso en el código de ejemplo, no puedo imaginar usar esta versión de la función. Esto es una lástima, porque eliminar el texto estándar de main habría simplificado el aprendizaje para los nuevos desarrolladores de Rust en el trabajo, y no habríamos necesitado explicar más quick_main! o el equivalente.

Casi las únicas circunstancias en las que puedo imaginar usar esto serían eliminar unwrap de doctests. Pero no sé si eso es compatible todavía.

Si usamos Display , podemos introducir un tipo rápido y sucio como MainError para acumular varios tipos de errores:

Personalmente, este tipo de solución agregaría la complejidad suficiente para que sea más fácil evitar ? -en- main completo.

@Aaronepower :

Por lo que puedo decir (podría estar totalmente equivocado ya que no tengo tiempo para compilar stdlib y no sé si la especialización cubre esto) no hay ninguna razón por la que no podamos agregar el siguiente impl a Terminación. Esto proporcionaría una forma ininterrumpida de usar Display cuando esté disponible y recurrir a Debug cuando no lo esté.

Si esto me permitiera escribir fn main() -> Result<(), failure::Error> y obtener un buen error legible por humanos usando Display , definitivamente satisfaría mis principales preocupaciones. Sin embargo, supongo que esto aún dejaría la cuestión de mostrar opcionalmente el seguimiento inverso en failure::Error cuando se establece RUST_BACKTRACE=1 , pero eso podría estar fuera del alcance, de todos modos, o al menos un problema que debería ser ocupado con failure .

Esto se estabilizó hace algún tiempo , y no creo que realmente podamos cambiar el comportamiento de Result . Personalmente, sigo bastante contento con el comportamiento actual para los casos de uso que tiendo a hacer (secuencias de comandos rápidas y sucias y similares), pero estoy de acuerdo en que las secuencias de comandos más complejas no lo querrán. Sin embargo, en el momento de la discusión, también discutimos varias formas de controlar ese comportamiento (parece que ahora no puedo encontrar esos comentarios porque github me está ocultando cosas).

Por ejemplo, podría definir su propio "envoltorio" para el tipo de error e implementar From para él:

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

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

Ahora puedes escribir algo como esto, lo que significa que puedes usar ? en main :

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

¿Quizás ese tipo debería ser parte de quick-cli o algo así?

@nikomatsakis Sí, entiendo totalmente esa solución, pero siento que eso anula el propósito de la concisión de usar ?-in-main . Siento que "es posible usar ?-in-main usando esta solución" socava ?-in-main desafortunadamente. Por ejemplo, no voy a escribir su solución en ejemplos sucintos y tampoco voy a imponer una dependencia de quicli para cada ejemplo que escriba. Ciertamente tampoco lo voy a usar para programas rápidos, porque quiero la salida Display de un error en básicamente todos los programas CLI que escribo. Mi opinión es que la salida de depuración de un error es un error en los programas CLI que se presenta a los usuarios.

La alternativa a ?-in-main es una función adicional de ~4 líneas. Entonces, si la solución para arreglar ?-in-main para usar Display es mucho más que eso, entonces personalmente no veo muchas razones para usarlo (fuera de las pruebas de documentos o pruebas unitarias, como mencionado anteriormente).

¿Es esto algo que se podría cambiar en la edición?

@BurntSushi ¿Cómo se sentiría si la solución alternativa estuviera en std , por lo que solo tendría que escribir una declaración de tipo de devolución un poco más larga en main() ?

@Kixunil Mi sensación inicial es que podría ser apetecible. Sin embargo, no lo he pensado demasiado.

@QuemadoSushi

La alternativa a ?-in-main es una función adicional de ~4 líneas.

En última instancia, el comportamiento es estable y no creo que podamos cambiarlo de manera realista en este momento. (Quiero decir que no es una violación de solidez o algo así).

Dicho esto, sigo encontrando la configuración actual bastante agradable y preferible a Display , pero supongo que es una cuestión de cuál desea que sea el "predeterminado", es decir, cualquiera de las configuraciones puede funcionar como la otro a través de varios newtypes. Entonces, por defecto preferimos scripts "rápidos y sucios", que quieren Debug , o productos finales pulidos. Tiendo a pensar que un producto final pulido es donde puedo permitirme una importación adicional lo suficientemente fácil, y también donde me gustaría elegir entre muchos formatos posibles diferentes (por ejemplo, ¿quiero solo el error o quiero incluir argv [0], etc.).

Para ser más concreto, imagino que quedaría así:

use failure::format::JustError;

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

o, para obtener un formato diferente, quizás haga esto:

use failure::format::ProgramNameAndError;

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

Ambos se sienten razonablemente bien en comparación con la función de 4 líneas que tuvo que escribir antes:

use std::sys;

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

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

Me estoy acercando a este tema ya que, para la calidad de la producción, agregar lo que equivale a un tipo de error personalizado para la salida parece una buena dirección para empujar a las personas de todos modos. Esto parece ser paralelo a cómo panic! , por ejemplo, proporciona mensajes de calidad de depuración de forma predeterminada.

@nikomatsakis Esa es quizás una mejor visión a largo plazo, y el resultado final me parece apetecible. Sería bueno si esos tipos de error terminaran en std algún día para que puedan usarse en ejemplos con la menor sobrecarga posible. :-)

Nota sobre el proceso: traté de enmarcar mi retroalimentación inicial aquí con "¿hay algo que se pueda hacer?" en lugar de "cambiemos algo que ya se ha estabilizado", en un esfuerzo por centrarme en el objetivo de alto nivel en lugar de pedir algo que ciertamente no puede hacer. :-)

Bueno, eso no tomó mucho tiempo: https://crates.io/crates/exitfailure 😆

Me pregunto cuántas personas se sorprenden al usar el rasgo Debug versus el rasgo Display . Tanto el borrador de discusión inicial como el RFC final usan Display . Esto es similar al incidente del rasgo impl universal, donde las personas pensaban que estaban obteniendo una cosa, pero solo sabían que estaban obteniendo otra después de que se estabilizaba. Además, teniendo en cuenta que mucha gente que está sorprendida no usará esta función, creo que no habrá una gran barra invertida si la cambiamos en la próxima edición.

@WiSaGaN Los detalles de un RFC pueden cambiar y cambiarán de vez en cuando. Esto es esperado y saludable. Los RFC son documentos de diseño y no una representación perfecta de lo que es y siempre será. El descubrimiento de información es difícil, y es mi culpa por no prestar más atención. Espero que podamos evitar volver a litigar la estabilidad de esta función y, en cambio, centrarnos en los objetivos de nivel superior en los que podemos actuar de manera factible.

@nikomatsakis El problema principal que veo al favorecer los casos rápidos y sucios es que ya hay una manera de abordarlos: .unwrap() . Entonces, desde esa perspectiva, ? es solo otra forma de escribir cosas rápidas y sucias, pero no hay una manera tan fácil de escribir cosas pulidas.

Por supuesto que el barco ha zarpado, así que estamos mayormente jodidos. Me encantaría ver esto cambiado en la próxima edición, si es posible.

@Kixunil , unwrap realmente no es una buena manera de abordar las cosas, como dice el título de este hilo, nos gustaría poder usar el azúcar sintáctico ? en main . Puedo entender de dónde viene (de hecho, fue parte de mi vacilación inicial con el uso de ? para el manejo de opciones), pero con la inclusión de ? en el idioma, este problema es un muy buena usabilidad ganar en mi humilde opinión. Si necesita una mejor salida, entonces hay opciones para usted. No lanza cosas a los usuarios sin probarlas primero, y el descubrimiento de que necesita un tipo personalizado para la salida de main será una realización bastante rápida.

En cuanto a "por qué" nos gustaría ? en main , considera lo raro que es ahora. Puede usarlo prácticamente en cualquier otro lugar (ya que tiene el control del tipo de retorno). La función main luego termina sintiéndose un poco especial, lo cual no debería.

.unwrap() es una solución rápida y sucia que no se compone, por lo que no puede factorizar el código dentro y fuera de main() mientras está esbozando un programa.

En comparación, ? es una solución rápida y sucia que compone, y hacer que no sea rápida y sucia es cuestión de poner el tipo de retorno correcto en main() , no de modificar el propio código.

@Screwtapello ah, ahora tiene sentido para mí. ¡Gracias!

Solo quiero expresar que pensé que el objetivo de esto era traer ? en main para todas las aplicaciones sin tener que recurrir a envoltorios adicionales... Si es solo para probar, no veo mucho beneficio en ello y seguiré apegado a .unwrap() , lamentablemente.

@oblitum No es solo para probar. Simplemente no (por defecto) va a usar el formato Display . Este puede o no ser el resultado que está buscando, pero casi definitivamente será mejor que el resultado de .unwrap . Dicho esto, podría hacer que su código sea "más agradable" para usar ? . En cualquier caso, todavía es posible personalizar la salida de ? errores en main como @nikomatsakis ha detallado anteriormente.

¿Sería posible introducir un nuevo tipo en el stdlib para la Vitrina? Algo como:

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

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

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

Esto debería permitir a los usuarios escribir:

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

Las principales motivaciones aquí son:

  • La pantalla se utiliza cuando se devuelve un error.
  • Los usuarios no tienen que hacer ningún ajuste adicional de sus tipos de error o resultado; solo pueden usar ? en su función principal.

Seguimiento: se me ocurre que la semántica de ? y From puede no permitir que esto funcione tan implícitamente como me gustaría. Sé que ? se convertirá entre tipos de error a través From , pero no sé si hace lo mismo para diferentes implementadores de Try . Es decir, se convertirá de Result<?, io::Error> a Result<?, FromIoError> , pero no necesariamente de Result<?, Err> a DisplayResult<Result<?, Err>> . Básicamente, estoy buscando algo como esto para trabajar:

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

Suponiendo que esto no funcione, ¿tal vez podría convertirse en un indicador de compilación condicional simple en Cargo.toml?

@Lucretiel : parece suponer que los programadores principalmente solo quieren imprimir un mensaje de salida en la consola. Esto no es válido para aplicaciones de escritorio no técnicas, demonios con conexiones de registro, etc. Prefiero que no añadamos cosas 'comúnmente útiles' a la biblioteca estándar sin pruebas convincentes (por ejemplo, números) de que debería estar allí.

Supongo que los programadores que devuelven Result<(), E> where E: Error desde main están interesados ​​en imprimir un mensaje de error en la consola, sí. Incluso el comportamiento actual, que es imprimir a través Debug , es "imprimir un mensaje de salida en la consola".

Parece haber un amplio acuerdo en este hilo de que algo debe imprimirse en stderr; la decisión clave que se debe tomar es "¿Debería imprimirse con "{}" o "{:?}" , o algo más, y qué tan configurable debería ser, en todo caso".

@Lucretiel , para mí, el valor de devolver un Result es controlar bien el estado de salida. ¿Debería dejarse en manos del manejador de pánico si imprimir algo? No solo está la cuestión de si imprimir algo en stderr, también qué imprimir (retroceso, mensaje de error, etc.). Consulte https://github.com/rust-lang/rust/issues/43301#issuecomment -389099936, especialmente. la ultima parte.

Usé esto recientemente y deseé que se llamara Display . Bastante seguro de que hicimos la llamada equivocada aquí.

Una falla del proceso que noté es que la decisión fue tomada por el equipo de lang (principalmente Niko y yo) en lugar del equipo de libs, pero el código en cuestión vive completamente en std. Posiblemente libs hubiera tomado una mejor decisión.

@sinbarcos ¿es demasiado tarde para cambiar? Noté que la característica todavía está etiquetada como solo nocturna/experimental en los documentos , pero es posible que no entienda parte de la granularidad detrás del proceso de estabilización.

A mí también me gustaría ver este cambio y lamento no haber sido un defensor más fuerte cuando el equipo me pidió que cambiara la implementación de Display a Debug .

El rasgo de @Lucretiel Termination es nocturno, pero la función en sí (que devuelve Termination implementaciones de main /pruebas) ya es estable.

¿Hay un boleto o una línea de tiempo para estabilizar el nombre del rasgo?

@xfix Eso implicaría que es posible cambiar la implementación, ¿verdad? El comportamiento no cambia ( Termination devuelto desde main ), pero el rasgo en sí mismo cambiaría de:

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

para:

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

es posible cambiar la implementación, ¿verdad?

No sin romper la compatibilidad con versiones anteriores. Este código se compila en el Rust estable de hoy:

#[derive(Debug)]
struct X;

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

Con el cambio propuesto para requerir Display , dejaría de compilarse.

Como ya se mencionó , es posible que algo como la especialización pueda ayudar, pero mi comprensión actual de la especialización es que solo sería útil para aquellos tipos que implementan tanto Display como Debug (es decir, tal que hay una implementación que es más especial).

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

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

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

patio de recreo

Esto puede ser suficiente para cubrir todos los casos comunes, pero expone públicamente la especialización.

Ah, ya veo a lo que te refieres con el óxido estable. Por alguna razón, no sabía que esto estaba disponible en estable.

¿Es esto algo que podría cambiarse en tiempo de compilación con una bandera Cargo? De la misma manera que otras cosas son conmutables, como panic = "abort" .

¿Quizás? Pude ver que es factible tener un comportamiento diferente para ? en main en otra edición de Rust. Probablemente no sea 2018, sin embargo, ¿tal vez 2021?

Diferentes cajas en el mismo programa pueden estar en diferentes ediciones pero usar el mismo std , por lo que los rasgos disponibles en std y sus impls deben ser los mismos en todas las ediciones. Lo que podríamos hacer en teoría (no estoy defendiendo esto) es tener dos rasgos, Termination y Termination2 , y requerir el tipo de retorno de main() para implementar uno o el otro dependiendo de la edición de la caja que define main() .

No creo que debamos desaprobar nada. Me haré eco del sentimiento de @Aaronepower de que limitarse solo a los tipos de visualización podría servir para insatisfechar a los usuarios de secuencias de comandos rápidas (derivar Debug es trivial, implementar Display no lo es) de la misma manera que la situación actual insatisfecha a los usuarios de producción. Incluso si pudiéramos, no creo que en última instancia sería un beneficio. Creo que el enfoque de especialización presentado por @shepmaster es prometedor, ya que crearía el siguiente comportamiento:

  1. Un tipo que tiene Depurar pero no Mostrar tendrá su salida de Depuración impresa de forma predeterminada
  2. Un tipo que tiene Depuración y Visualización tendrá su salida de Visualización impresa de manera predeterminada
  3. Un tipo que tiene Mostrar pero no Depurar da como resultado un error del compilador

Alguien podría objetar el punto 2, en el caso de que tenga un tipo con Display pero quiera que imprima Debug por cualquier motivo; Creo que este caso se abordaría con la propuesta de Niko de varios tipos de error de formato de salida prediseñados (que probablemente valga la pena explorar incluso si optamos por la ruta de especialización).

En cuanto al punto 3, es un poco tonto (¿quizás deberíamos haber hecho trait Display: Debug en 1.0?), pero si alguien se ha tomado la molestia de escribir una impl de Display, entonces no es mucho pedir que lo abofeteen. una única derivación (que, sospecho, probablemente tengan de todos modos... ¿hay alguien por ahí que se oponga en principio a tener Depuración en su tipo de pantalla?).

limitarse solo a los tipos de visualización podría servir para insatisfechar a los usuarios de secuencias de comandos rápidas (derivar la depuración es trivial, implementar la visualización no lo es)

Un caso en el que Display es preferible a Debug en secuencias de comandos rápidas es usar &'static str o String como tipo de error. Si tiene un tipo de error personalizado para colocar #[derive(Debug)] , ya está gastando algo de energía no trivial en el manejo de errores.

@SimonSapin No diría que es preferible en un entorno de secuencias de comandos rápido, o más bien no es lo suficientemente preferible como para estar en una posición en la que quería que la cadena se imprimiera como Display formato sin querer hacer el esfuerzo tener errores correctamente formateados, para mí es preferible Debug porque el formato de error cuando es una cadena no está bien formado, por lo que tener Err("…") a su alrededor me permite distinguir visualmente el error de cualquier otro salida que puede haber sido emitida.

FWIW lo que está formateado no es el Result<_, E> , solo el E dentro. Entonces no verá Err( en la salida. Sin embargo, el código actual agrega un prefijo Error: , que presumiblemente se mantendría si se usa Display . Además de no imprimir comillas, Display tampoco haría una barra invertida de escape del contenido de la cadena, lo cual es deseable en mi opinión cuando se trata de mostrar un mensaje (de error) a los usuarios.

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

Creo que dada la situación, lo mejor que podemos hacer es la especialización para los tipos que implementan Display + Debug para imprimir el Display impl. Es desafortunado que los tipos que no implementan Debug no se puedan usar como errores (hasta que admitamos impls de intersección), pero es lo mejor que pudimos hacer.

La pregunta principal es si ese impl cae o no dentro de nuestra política actual para impls especializados en std. Por lo general, tenemos una regla de que solo usamos la especialización en std donde no proporcionamos una garantía estable de que el comportamiento no cambiará más tarde (porque la especialización en sí misma es una característica inestable). ¿Nos sentimos cómodos proporcionando este impl ahora sabiendo que es posible que algún día estos tipos vuelvan a imprimir su salida Debug si eventualmente eliminamos la especialización como una característica?

En realidad, creo que no podemos permitir esta especialización en este momento, porque podría usarse para introducir falta de solidez. ¡Este impl es exactamente el tipo de especialización que nos causa tantos problemas!

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

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

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

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

Debido a la forma en que funciona actualmente la especialización, podría llamar a report en un Foo<'a, 'b> donde 'b no sobrevive a 'a , y actualmente es totalmente posible que el compilador seleccionará la impl que llama a la instancia Display aunque no se cumplan los requisitos de duración, lo que permite al usuario ampliar la duración de una referencia de forma no válida.

No sé sobre ustedes, pero cuando escribo "guiones" rápidos, solo unwrap() la mierda de todo. Es 100 veces mejor, porque tiene backtraces que me brindan información contextual. Sin él, en caso de que tenga let ... = File::open() más de una vez en mi código, ya no puedo identificar cuál falló.

No sé sobre ustedes, pero cuando escribo "guiones" rápidos, solo unwrap() la mierda de todo. Es 100 veces mejor, porque tiene backtraces que me brindan información contextual. Sin él, en caso de que tenga let ... = File::open() más de una vez en mi código, ya no puedo identificar cuál falló.

En realidad, esta es la razón por la que me siento tan convencido con Display , ya que generalmente lo que hago en este caso es un map_err que adjunta el nombre del archivo y otra información contextual relevante. En general, la salida de depuración es menos útil para rastrear lo que realmente salió mal, según mi experiencia.

@Kixunil : Alternativamente, preferiría una estrategia de manejo de errores más elaborada y/o un registro y/o depuración más extensos.

Supongo que "Implementar el RFC" se puede marcar, ¿verdad? ¡Al menos según esto ! :sonreír:

Perdóneme si esto no tiene ni idea, pero ¿no podría usar la especialización para informar la cadena de errores cuando sea posible?

use std::error::Error;

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

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

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

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

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

¿Hay alguna razón por la que no se implementó esta parte del RFC?

Hicimos algunos cambios en los conjuntos precisos de rasgos e impls que se usarán, aunque no recuerdo los detalles en este momento, y creo que quedan algunos detalles por estabilizar. Es posible que desee consultar los informes de estabilización vinculados desde el tema principal para obtener más detalles.

¿Cuál es el estado de esta característica? ¿Hay una propuesta de estabilización?

@GrayJack Este problema debería cerrarse, ya que llegó hace bastante tiempo.

Cierto, y me confundí un poco, llegué aquí desde el enlace en el rasgo Termination y estaba preguntando sobre eso, ¿hay un problema adecuado para termination_trait_lib ?

Este tema debe estar cerrado

Este problema aún está abierto para rastrear la estabilización de Termination .

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