El núcleo de Rust
(jyn.dev)- Rust es un lenguaje donde varios conceptos están estrechamente entrelazados entre sí, por lo que incluso para entender un programa básico hay que aprender muchos elementos al mismo tiempo
- Funciones, genéricos, enums, pattern matching, traits, referencias, ownership,
Send/Sync,Iterator, etc., son todos elementos centrales diseñados para interactuar entre sí - En comparación con JavaScript, en JS se puede escribir código con solo conocer algunos conceptos, pero en Rust solo es posible escribir código realmente significativo cuando se entiende el contexto completo del lenguaje
- Esta complejidad de Rust eleva la barrera de aprendizaje, pero al mismo tiempo ofrece seguridad y consistencia, y tiene un gran impacto en la forma de diseñar el código
- Esta estructura del lenguaje es lo que hace especial a Rust, y la visión de un “Rust más pequeño” nos hace volver a pensar en una filosofía de lenguaje cuidadosamente ensamblada
La dificultad de aprender Rust
- Aunque Rust tiene una barrera de entrada alta, muchas personas han contribuido a mejorar la documentación, las APIs y los diagnósticos
- Entre sus conceptos básicos están funciones como objetos de primera clase, enums, pattern matching, genéricos, traits, referencias, borrow checker, seguridad en concurrencia e iteradores
- Estos conceptos dependen unos de otros y están entrelazados, por lo que es difícil aprenderlos por separado, y la biblioteca estándar también aprovecha la mayoría de estas funciones
- Incluso para entender unas 20 líneas de código Rust, hay que captar al mismo tiempo varios elementos como el paradigma funcional,
Resulty el manejo de errores, tipos genéricos, enums e iteradores
Comparación entre Rust y JavaScript
- Al escribir el mismo programa de detección de cambios en archivos en Rust y en JS, en Rust se entrelazan numerosos conceptos del lenguaje
- En JS, básicamente basta con entender las funciones y el manejo de null para poder escribir código funcional
- Esto no significa simplemente que Rust sea más difícil, sino que muestra que Rust es un diseño que exige una comprensión estructural del lenguaje en su conjunto
El diseño estrechamente acoplado de Rust
- El núcleo de Rust es la combinación de funciones diseñadas de forma orgánica
- Los enums resultan incómodos sin pattern matching, y el pattern matching también es limitado sin enums
ResulteIteratorno pueden implementarse sin genéricos- Los conceptos
Send/Syncy las restricciones deprintlnsolo pueden expresarse de forma segura con traits - El borrow checker garantiza la seguridad de
Send/Syncmediante el análisis de captura de closures
- Este acoplamiento convierte a Rust no simplemente en un conjunto de funciones, sino en un sistema de lenguaje integrado
La visión de un Rust más pequeño
- En 2019, without.boats mencionó “Smaller Rust” y discutió la posibilidad de un Rust pequeño y refinado
- Hoy Rust es mucho más grande, pero la idea de un Rust pequeño nos recuerda la esencia de un diseño de lenguaje cuidadosamente engranado
- El atractivo de Rust está en que sus elementos del lenguaje son independientes entre sí y, al combinarse, ofrecen una poderosa expresividad y seguridad
Conclusión
- Rust es difícil de aprender, pero la consistencia e integración de sus conceptos entrelazados funcionan como una gran fortaleza
- Gracias a esta estructura, Rust hace que los desarrolladores no solo escriban código, sino que también adopten una forma de pensar que considera al mismo tiempo la seguridad y el rendimiento
- La esencia de Rust está en un “lenguaje central pequeño y sofisticado”, y esa sigue siendo una filosofía importante incluso en el Rust ampliado de hoy
1 comentarios
Opiniones de Hacker News
fs.watchse indica explícitamente que en el callback hay que verificar obligatoriamente sifilenamepuede sernull. En Rust, este hecho quedaría reflejado en el sistema de tipos y te obligaría a manejarlo, pero en JS es muy fácil escribir código al tanteo. Documentación relacionadanullse vuelve obligatoria. Por eso me parece un buen ejemplo de cómo TS es un paso relativamente liviano que acerca a JS un poco más a la corrección del lado de Rustfor path in pathsdebería serfor (const path of paths). En JS, sin paréntesis da error de inmediato, pero la diferencia entreinyofes que recorre índices del iterable, no valores, así que en la práctica terminaría pasando el índice convertido astringcomo primer argumento defs.watch. Incluso TypeScript podría no detectar este errorkind. Enconsole.log("${kind} ${filename}")debería sereventType(una cadena), nokindprintlnde Rust solo puede imprimir tipos que implementan los traitsDisplayoDebug. Por esoPathno se puede imprimir directamente. No todos los sistemas operativos guardan rutas compatibles con UTF-8, mientras que todos los tiposstringde Rust son UTF-8. O sea, imprimir unPathpuede implicar pérdida de información.Pathdevuelve mediante el métododisplayun tipo que implementaDisplay. Rust integró esto en el sistema de tipos, pero en JS/TS es difícil expresar que internamente las cadenas son UTF-16, y para manejar correctamente rutas no Unicode hay que usar directamenteTextEncoder/TextDecoder. Por experiencia pasada, si un servidor enviaba texto en Shift_JIS y lo leías conresponse.text(), en runtime solo salía una cadena vacía. Si no estás acostumbrado a los problemas de codificación, puedes pasarte días depurando algo así. Y el ejemplo en JS tiene bugs y errores de sintaxis que no están en el código Rust (en el bucle hace faltafor-ofen vez defor-in). Tampoco diría que este ejemplo use solo "funciones de primera clase"; igual que en Rust, requiere entender iteradores, y además usa CommonJS. También hay que aprenderasync/await,Promisesytop-level await, y este último recién fue soportado hace poco en algunos runtimes, incluido node. Aún hay motores JS que no lo soportan (por ejemplo, Hermes de React Native)Ese tipo de cosas es la razón por la que sigo usando Rust. Es solo un ejemplo, pero estos problemitas y trampas están por todos lados en otros lenguajes. Puede que cada uno por separado no ocurra, pero si se acumulan a lo largo de la vida del programa, los bugs raros empiezan a aparecer de la nada y uno tiene que seguir cazándolos sin parar. En Rust eso no pasa. El sistema de tipos bloquea de antemano una cantidad absurda de casos. De hecho, cuando sacas software hecho en Rust con la funcionalidad ya completa, luego solo agregas features de vez en cuando y casi desaparece el trabajo típico de corregir bugs. Claro, siempre puede haber errores lógicos en cualquier parte, pero te corta de raíz los problemas causados por desajustes tontos de tipos o estructura que en otros lenguajes son comunes, así que la productividad y el mantenimiento se sienten completamente distintos
Personalmente siento que no hay tantos desarrolladores JS/TS que entiendan de verdad
thenable/Promiseyasync-await. Incluso he visto cosas como esto:Envueltan tal cual un wrapper con callbacks dentro de una
Promisey luego lo vuelven a usar dentro de una funciónasync. Cada vez que veo esto me duele el alma. En serio he visto código así por todos lados. Y si además sumas imports de módulos,import()asíncrono, transpilación, code splitting, etc., la cosa se vuelve realmente complejarustfmt,rust-analyzer, corregir bugs enrustcy mejorar los reportes de error en Cargo. Yo mismo escribo todos los días scripts de reproducción de issues con cargo script-Zscripty me distraje con eso. Lleva en marcha desde 2023 y hay issues abiertos que ya parecen bastante cerca de completarse. También vi en el repositorio de ZomboDB que manejan el pipeline de build en rust, aunque no entendí por completo todo el contexto. Quiero mencionar que el frontmatter de cargo es increíblemente útil para la portabilidad de scripts. Solo hace falta compartir un archivo, y a diferencia de Python o Node.js, puedes traer dependencias y usarlas de inmediato sin instalación ni inicialización adicional#!/some/pathhace que el shell simplemente le pase el archivo completo por stdin al comando indicado y lo ejecuteasyncyconst. Entonces, me habría gustado que dijera más directamente: "Rust antes deasyncyconstera más pequeño y más limpio", porque el texto no lo explica así de frenteCopy, el reborrowing, la coerción porderef, elinto_iterautomático en bucles, la llamada automática adropal terminar el scope (podrías llamarlo tú mismo o hacer que el compilador dé error), el:Sizedimplícito por defecto en trait bounds, la omisión de lifetimes (lifetime elision), la ergonomía dematchy otras automatizaciones/conveniencias, podrías tener un Rust realmente más simple en términos mecánicos. Pero un lenguaje así sería muy incómodo para usarlo en el día a día. Irónicamente, muchos de esos elementos en realidad fueron diseñados para principiantesasyncyconst. No lo expresé de forma directa porque tengo amigos que trabajaron en esas features. Matklad lo expresó muy bien en lobste.rs: el Rust de 2015 era más acabado y más coherente, pero la visión de Rust no es la coherencia total, sino convertirse en un lenguaje útil para la industriaDeref..into()y el traitFrommanejan las conversiones de tipo de una forma demasiado silenciosa. Incluso en la biblioteca estándar hay muchas funciones de "conveniencia" de este tipo. Al final, el tipo real del objeto se vuelve ambiguo y cuesta más conectar una llamada de función con su implementación (aunque claro, el IDE ayuda un poco)implicit return) oculta el flujo del programa y lleva a errores. El operador de interrogación tampoco me convence muchomove assignmento el significado de la keywordconsten Rust incluso podrían reducir luego el esfuerzo de desaprender malos hábitos adquiridos en lenguajes tradicionalesnone" pueden sentirse mucho más difíciles que encontrar directamente la línea de un crash en runtime con un caso de prueba. Si vas imprimiendo valores línea por línea para hacer troubleshooting, la mayoría de las veces lo resuelves rápido, pero si te bloqueas con un error críptico del compilador puedes perder muchísimo tiempomem, así que si quieres entender bien cómo está estructurada la interfaz, conviene empezar por std::mem