Dejar el código WIP como advertencias en Rust
(blog.dureuill.net)- Durante el desarrollo en Rust, resolver de inmediato todas las rutas de error y los problemas de ownership puede interrumpir fácilmente el flujo de implementación, por lo que resulta útil una estrategia de crear primero el happy path y rastrear el trabajo pendiente como advertencias
- Solo con Rust estándar, se puede marcar código incompleto combinando
unwrap,clone,todo!, las advertencias predeterminadas del compilador y un truco con comentarios de documentación/// - Los métodos existentes tienen limitaciones:
unwrapycloneno generan advertencias,todo!también depende de advertencias indirectas, y el truco de///puede terminar en errores de compilación o documentación no intencional según su ubicación - El crate
wipexpone siempre las implementaciones temporales, losunwrap/clonetemporales y las notas para corregir más adelante como advertencias mediantewip!,unwrap_wip,clone_wipyfixme! - Meilisearch convierte las advertencias en errores en CI con
-D warningpara evitar que entre código WIP, y usa un flujo en el que la dependenciawipagregada durante el desarrollo se elimina al limpiar el historial antes de la revisión
Por qué se necesitan advertencias durante el desarrollo en Rust
- Rust es un lenguaje que prioriza la corrección, pero manejar de inmediato todas las rutas de error durante el desarrollo puede reducir la velocidad de implementación y la concentración
- Cuando
rustcbloquea la compilación por motivos como devolverResult, manejar errores o implementar edge cases, normalmente aparecen dos opciones- Implementar de inmediato un manejo de errores completo y refactorizar varias veces antes de preparar el PR
- Dejar una marca del tipo
TODO later, bajar el problema al nivel de advertencia y continuar con el trabajo actual
- Las advertencias no bloquean el desarrollo, pero quedan como señales de que algo debe limpiarse antes de fusionar el PR
- Meilisearch ejecuta el compilador en CI con el flag
-D warning, convirtiendo todas las advertencias en errores, para evitar que el código WIP llegue accidentalmente a producción - La revisión de código es una práctica básica para evitar bugs y compartir conocimiento dentro del equipo, pero si se depende solo de la memoria y la atención humanas, tarde o temprano puede haber errores, por lo que se necesitan guardrails adicionales
Formas de posponer el tratamiento de código incompleto con Rust estándar
-
Posponer el manejo de errores con
unwrap- Usar
unwrapen unResultoOptionpermite extraer el valor interno y continuar con la implementación actual, a cambio de provocar un panic en la ruta de error - Una vez que el resto del código está listo, se pueden localizar y eliminar los
unwrapagregados temporalmente, reemplazándolos por un manejo de errores adecuado - Este enfoque se basa en la premisa de que, aunque Rust proporcione una estructura para manejar correctamente los errores, no es necesario terminar ese manejo de inmediato
- Usar
-
Posponer problemas de ownership con
clone- Los problemas de ownership son menos comunes que el manejo de errores, pero mantener el ownership correcto puede requerir cambios de diseño
- En ese caso, se puede hacer
clonedel valor en vez de moverlo, evitando temporalmente problemas de lifetime - Más adelante, se pueden identificar empíricamente casos en los que es posible eliminar el clone pasando la variable adecuada a un nivel más alto del stack
- Sin embargo, como la corrección puede llevar a cambios en la estructura del código, posponer esto es más difícil que posponer el manejo de errores
-
Reservar el lugar de una implementación con
todo!()- La macro
todo!()de la biblioteca estándar de Rust indica código incompleto y es útil como placeholder para pasar el análisis de tipos durante el prototipado - Como
todo!()diverge con un panic, puede usarse como expresión placeholder en la mayoría de las posiciones de tipo - Permite usar definiciones de funciones mientras se está definiendo la interfaz, dejando la implementación real para después
todo!()se parece más a una comodidad WIP para quienes escriben y revisan el código que a una forma de informar a los usuarios del programa que una función no está implementada- Para informar a los usuarios que una funcionalidad no está implementada existe por separado la macro
unimplemented!()
- La macro
-
Advertencias predeterminadas del compilador y el truco de comentarios de documentación
- Las advertencias predeterminadas del compilador de Rust también son útiles para revelar estados incompletos durante el desarrollo
- Bindings no usados
mutinnecesariosResultyOptionno usados
- Si durante el desarrollo se eliminan temporalmente estas advertencias con un prefijo
_u otros trucos, existe el riesgo de que el código incompleto pase sin ser detectado - Los edge cases o la lógica rota que no se rastrean con el sistema de tipos a veces se dejan como comentarios
- Si se escribe un comentario de forma que parezca un comentario de documentación
///, Rust emite una advertencia cuando no hay un objetivo de documentación, por lo que el problema en esa ubicación puede rastrearse como advertencia
- Las advertencias predeterminadas del compilador de Rust también son útiles para revelar estados incompletos durante el desarrollo
Límites de los métodos estándar
- El mayor problema es que no todas las técnicas generan de forma estable una advertencia conectada con el problema que debe resolverse
todo!()suele provocar advertencias, pero muchas veces vienen de causas indirectas, como argumentos de función no usados o mutaciones necesarias que no ocurrenunwrapycloneno generan ninguna advertencia, así que hay que volver a leer el código antes del PR final para eliminarlosunwraptambién puede quedar legítimamente en producción, por lo que se pueden mezclar el uso temporal para WIP y el uso normal- El truco de comentarios de documentación
///se acerca a un hack, y en posiciones de expresión los comentarios de documentación están prohibidos, por lo que puede causar errores de compilación - Si se usa
///en una ubicación que realmente se documenta, no aparece la advertencia esperada y puede quedar documentación no deseada en el código distribuido
Herramientas que ofrece el crate wip
- El crate
wipes un conjunto de herramientas para hacer más cómoda la estrategia de “advertencias para tratar más adelante” durante el desarrollo en Rust - Se puede agregar como un crate normal
cargo add wip
- En los archivos que lo necesiten se puede importar el prelude
use wip::prelude::*;
wipdepende de las deprecation warnings de Rust, por lo que puede ser menos molesto desactivar en el editor la función que tacha las llamadas a funciones deprecated-
wip::wip!- La macro
wip!funciona comotodo!, pero al usarse siempre emite una advertencia - Puede usarse en los lugares donde antes se usaba
todo!para marcar código incompleto - En la versión inicial se llamó
todo, pero cuando una dependencia intentaba hacer shadow de la macro destd, Rust no lo favorecía, lo que causaba problemas al usar el prelude
- La macro
-
unwrap_wipyclone_wipunwrap_wipyclone_wipestán implementados como extension traits paraResultyOption- Si están en scope, funcionan como
unwrapyclonenormales, pero emiten una advertencia al usarse - Permiten señalar claramente que no se pretende dejar
unwrapocloneen el código final
-
wip::fixme!- La macro
fixme!es una variante non-panicking dewip!y no sustituye una expresión - Puede usarse como un comentario TODO, pero genera advertencias de forma estable sin el truco de
/// - Se usa con frecuencia para dejar notas sobre cosas que deben corregirse más adelante
- La macro
Flujo de uso y precauciones
- El crate
wipse usó recientemente en algunos PR, incluido uno grande: Meilisearch PR #6484 - El flujo habitual consiste en agregar la dependencia
wipa un crate del workspace durante el desarrollo y eliminarla al reescribir el historial antes de la revisión wipayuda a concentrarse en el happy path sin olvidar el trabajo que debe hacerse después- También tiene desventajas
- Si al agregar un
fixmeno se escribe suficiente contexto, puede ser difícil corregirlo más adelante - En PR grandes, la cantidad de advertencias generadas por
wippuede crecer y volverse pesada
- Si al agregar un
- La API actual puede consultarse en la documentación de
wipen docs.rs
1 comentarios
Opiniones en Lobste.rs
Sinceramente, al ver la justificación de
wip, me da la impresión de que reinventaron Clippy por no entender cómo se supone que deben usarse originalmente las herramientas.Los desarrolladores de la toolchain han distinguido qué se convierte en una advertencia de
rustcy qué se convierte en un lint de Clippy, perowipparece recrear funcionalidades existentes con otro nombre y forzar lints que estaban encargo clippyhaciacargo checkycargo build.Por ejemplo, la explicación de
wip!consiste en hacer que#![warn(clippy::todo)]aparezca también fuera decargo clippy, a cambio de agregar una nueva dependencia y sintaxis no estándar; yunwrap_wipse parece a#![warn(clippy::unwrap_used)].Para mí,
.unwrap()es para prototipado, así que activo#![warn(clippy::unwrap_used)], mientras que.expect("description of invariant")es para producción, donde podría usarse.unwrap()en vez de.unwrap_wip(), así que no activo#![warn(clippy::expect_used)]. Ej.:.expect("parse regex from const").No estoy tan seguro sobre
clone_wipyfixme!, y no he pensado lo suficiente como para responder cómo pretendían los desarrolladores que se manejaran esos casos.Dicho eso, es cierto que en el artículo falta una discusión sobre linters existentes como Clippy. También tiene que ver con mi forma personal de usarlo: antes ejecutaba Clippy al guardar, pero en nuestro proyecto era demasiado lento, tardaba minutos entre guardados, así que dejé de hacerlo; ahora solo lo ejecuto en CI y en un hook previo al push.
Llevar las advertencias a
rustces mucho más rápido y permite ver advertencias inline durante el trabajo iterativo, lo cual se vuelve central para este workflow.La configuración de Clippy también era bastante engorrosa, al menos la última vez que la revisé. No había una forma de especificar lints en el código de una sola vez para todos los crates del workspace, así que había que agregar un
-Dexplícito en CI por cada lint que se quisiera rechazar en el workspace, o seguir poniendo atributos globales en todos los crates.#![warn(clippy::todo)]se necesita en todos los crates, así que para nosotros no supone una gran ventaja frente a agregar/quitar un crate solo durante el desarrollo. Creo quetodo!debería advertir por defecto enrustco en Clippy, y coincido en que, si eso pasara, la utilidad dewipdisminuiría.No estoy de acuerdo con que
unwrap_wipsea#![warn(clippy::unwrap_used)]por dos motivos. Primero, desde el punto de vista de la intención, la distinción “unwrapes no producción,expectes producción” es común, pero no universal. Segundo, en la base de código existente hay aproximadamente 4600 unwrap, y aunque podríamos cambiarlos todos porexpect, eso nos obligaría a poner mensajes temporales por todas partes o a dedicar demasiado tiempo a reconstruir precondiciones como “este mutex no fue poisoned” o “quiero propagar esta falla al caller”.fixme!es muy útil y me gustaría que entrara en la biblioteca estándar o algo similar. De hecho, es la función que más uso de este crate.El artículo no cubre estos detalles, pero como este crate no es estándar sino una biblioteca especializada, también tiene funciones como
wip_iter. Permite usar macros tipotodoen funciones conimpl Traiten posición de retorno de iteradores;wip_futurehace lo mismo. Me ha pasado bastantes veces que la inferencia del tipo de retorno ytodono funcionaban juntos y tenía que fabricar un tipo a la fuerza. Alguien también propuso que en modo release produzca un error de compilación por defecto, y voy a revisarlo junto con otras ideas de mejora.Para equilibrar un poco el costo de una nueva dependencia y una sintaxis no estándar: es una dependencia de un solo archivo que se usa solo durante el desarrollo y que normalmente se elimina al terminar.
El problema de prototipar con
unwrapes que, cuando después quieras introducir errores correctamente, puede cambiar la forma del tipo de retorno, y eso genera efectos en cadena, sobre todo si esas funciones se usan en estilo funcional.Recomiendo empezar con
ResultusandoBox<dyn Error>, strings,anyhow, etc. Después se puede cambiar a un enum de errores real, pero es más difícil refactorizar más tarde código que parece no fallar para que devuelvaResult.Además, si usas LLM, hay que tener cuidado. Un LLM es una máquina de reconocer patrones, así que no distingue entre “lo hice a propósito de forma descuidada y quiero arreglarlo después” y “así es como este proyecto debe manejar errores”. Con el tiempo puede multiplicar las decisiones de “solo por ahora”, así que ayuda dejar en
mainuno o dos comentarios que indiquen dónde/cuándo está bien un manejo de errores laxo.La documentación y yo no estamos de acuerdo con la interpretación de
todo!()yunimplemented!(). Aun así, el siguiente párrafo que omitiste equilibra un poco esa postura.La documentación dice que
todo!comunica la intención de implementarlo más adelante y que el mensaje es “not yet implemented”, mientras queunimplemented!no hace esa promesa y el mensaje dice “not implemented”.Cuando
todo!se agregó en la versión 1.40.0, era simplemente un nombre más corto y no tenía diferencias conunimplemented!salvo el nombre.Recién en 1.42.0 se quitó el “yet” del mensaje de
unimplemented. Según recuerdo, definir este significado para las dos macros también fue algo polémico, al igual que lanzartodo!como una forma abreviada deunimplemented!.Hoy en día se recomienda
unimplemented!()para casos como implementaciones de traits en las que, por razones técnicas, hay que proporcionar el cuerpo de un método, pero nunca debería llamarse. El ejemplo de la documentación es justamente de ese tipo.Me pregunto si el flujo de agregar la dependencia
wipa los crates del workspace durante el desarrollo y luego reescribir el historial antes de la revisión para quitarla podría hacerse más fluido con herramientas adicionales.Me incomoda un poco tener que reescribir el lock file al modificar el medio de un PR. Si mientras tanto el árbol de dependencias también cambió en otro lado, pueden aparecer conflictos innecesarios. Quizás se podría abusar de
jj fix. En cualquier caso, la idea del proyecto es muy buena y creo que terminaré usándola en algún lugar.Una macro para notas que no provoque panic, como
wip::fixme!, es una idea realmente buena. El truco del comentario de documentación también es una forma elegante que conviene recordar.Hay ideas interesantes que ojalá deriven en cambios en
rustc.todo!()realmente debería generar una advertencia por defecto.Sería aún mejor que los desarrolladores pudieran desactivar estas advertencias cuando están en modo hackeo. Demasiadas advertencias abruman, y normalmente ejecuto
cargoen una consola de 19 líneas en la parte inferior del IDE. Durante el desarrollo, las advertencias tapan los errores reales y las advertencias más importantes. Con solo tener la disciplina de ordenar siempre los errores al final, este problema se mitigaría.Además, los nombres
.unwrap_wip()y.clone_wip()son más largos que los originales, así que se sienten demasiado extensos. Recomendaría nombres como.du()y.dc(), abreviaturas de “dev unwrap” y “dev clone”. Serían más fáciles de usar en programación rápida.¿No se podría agregar automáticamente
.clone()y.unwrap()donde haga falta? Por ejemplo, con un flag o una función como#[wip(autoclone,autounwrap)]. Probablemente no se pueda, pero se vale soñar.Cada vez que empiezo un proyecto nuevo o un prototipo, pongo estas líneas en
main.rs:#![allow(dead_code)]#![allow(unused_variables)]#![allow(unreachable_code)]En las primeras etapas reducen mucho el ruido.