- Después de pasar de JavaScript a Rust para enfocarse en WebAssembly, durante 3 años evaluó el valor real de Rust creando Wick, despliegues en producción, un ebook y alrededor de 100 paquetes en crates.io
- El borrow checker, el rico sistema de tipos, los patrones funcionales y la ausencia de
nullevitan muchos errores en tiempo de compilación y permiten mantener grandes bases de código con pocas pruebas - Clippy y Cargo workspace son potentes, pero las brechas en las herramientas y el ecosistema, como la configuración global de lint y el despliegue de workspaces, terminan convirtiéndose en costos operativos
- async, el refactoring y la gestión de generic·lifetime·trait constraints siguen siendo áreas con más fricción que en JavaScript o Go
- Rust es sólido y versátil, pero el costo de contratación, aprendizaje, iteración rápida y rastreo de problemas es alto, así que encaja mejor cuando el alcance está claro o se puede absorber el costo inicial
WebAssembly impulsó la elección de Rust
- Hace algunos años dejó de lado su trabajo anterior para enfocarse 100% en WebAssembly, y en ese momento Rust era el lenguaje con mejor soporte para compilar a WebAssembly
- Los runtimes de WebAssembly con más funcionalidades también estaban basados en Rust, así que entre las opciones disponibles Rust era la alternativa más realista
- Después creó Wick, un framework de aplicaciones y runtime que usa WebAssembly como sistema central de módulos
- A lo largo de 3 años acumuló experiencia con Rust pasando por varios despliegues en producción, un ebook y la publicación de alrededor de 100 paquetes en crates.io
Mantener más código con menos pruebas
- En Rust empezó escribiendo pruebas como en cualquier otro lenguaje, pero luego descubrió que estaba escribiendo pruebas que no podían fallar si el código compilaba
- Si se evitan bloques
unsafe {}y métodos propensos a panic como.unwrap(), muchos problemas se esquivan de forma predeterminada - El borrow checker, el rico sistema de tipos, los patrones y bibliotecas funcionales, y la ausencia de valores
nullreducen el esfuerzo dedicado a las pruebas - En el proyecto Wick mantuvo más de 70,000 líneas de código con muchas menos pruebas de las que habría necesitado en otros lenguajes
- Cuando sí hacen falta pruebas, el harness de pruebas de integración de Rust permite añadirlas fácilmente junto al código
Rust también cambió los hábitos de programación en otros lenguajes
- El compilador de Rust seguía quejándose de código que en otros lenguajes se consideraba normal, y ese proceso terminó cambiando sus hábitos de programación
- Ahora incluso en otros lenguajes le incomoda cuando el orden de las líneas se siente extraño o no se revisan los valores de retorno
- También siente un rechazo mucho más fuerte que antes ante los errores en tiempo de ejecución
- La rigurosidad de Rust es incómoda, pero una vez que uno se acostumbra a la protección del compilador, se vuelve difícil regresar a otros lenguajes
Clippy es útil para mucho más que linting
- Clippy es el linter de Rust, pero está más cerca de ser una herramienta de apoyo amigable que de un simple verificador, porque sugiere código alternativo
- La biblioteca estándar de Rust es muy grande, y con tipos, traits, macros y funciones repartidos en muchas partes, puede ser difícil encontrar la API que se necesita
- Varias reglas detectan patrones comunes que pueden reemplazarse mejor con métodos o tipos de la biblioteca estándar
- Ejemplo: manual_is_ascii_check
- Cientos de reglas cubren rendimiento, legibilidad e indirección innecesaria, y cuando es posible también ofrecen código alternativo
- La configuración global de lint a nivel de proyecto parecía llegar a través de un issue de Cargo, pero hasta entonces Wick tuvo que actualizar automáticamente con scripts la configuración inline de lint en decenas de crates
El ecosistema tiene vacíos que hay que asumir
- El problema de la configuración global de Clippy es solo un ejemplo de los vacíos del ecosistema que aparecen con frecuencia en las herramientas y bibliotecas de Rust
- Los issues relacionados ya están cerrados, pero permanecieron abiertos durante años y tardaron mucho en resolverse
- Rust sigue atrayendo nuevos usuarios hasta ser elegido durante mucho tiempo como “el lenguaje más amado”, pero ese flujo no se tradujo de inmediato en mejoras drásticas en bibliotecas y herramientas
- Era común que aparecieran forks puntuales para cubrir casos de uso específicos, y en Wick también intentaron enviar PRs pero se toparon con situaciones parecidas
- Entre las posibles razones están la presión por mantener APIs estables y un sistema de tipos muy detallado
- A los dueños de bibliotecas les cuesta aceptar incluso cambios pequeños porque pueden terminar implicando un cambio de versión mayor
- También es alta la carga de escribir código Rust que satisfaga las necesidades de todo el mundo
Cargo, crates.io y la fricción al publicar workspaces
- La estructura del repositorio de Wick se diseñó tomando como referencia proyectos populares y al principio parecía razonable, pero los problemas aparecieron en la fase de publicación
- Con Cargo es fácil compilar, probar y usar crates del tamaño de un módulo, pero publicar en crates.io es otro asunto
- En crates.io, todos los crates referenciados deben estar publicados individualmente para poder hacer publish de un paquete
- Tiene sentido impedir la publicación de un crate que depende de paquetes que solo existen en el sistema de archivos local
- Pero en una estructura natural donde un proyecto grande se divide en pequeños módulos internos, no se pueden publicar sub-crates que solo existen dentro del crate padre incluyéndolos como parte de este
- También hubo una corrección: un crate con local dev dependency puede publicarse si incluye
versionenCargo.toml - El soporte de Cargo para workspaces en sí es excelente, y la experiencia de gestionar proyectos grandes es mejor que en la mayoría de los lenguajes
- Sin embargo, el workspace no resuelve el problema de publicación, y aunque existan varias formas de configurarlo, no es fácil encontrar una “respuesta correcta” que publique sin fricción
- El hecho de que existan muchos crates utilitarios relacionados con cargo workspace publish ya muestra el problema
- Al publicar Wick, a menudo tardaban más de 1 hora combinando tareas manuales repetitivas con herramientas que solo funcionaban parcialmente
async es una de las mayores fuentes de fricción
- El async de Rust se siente como una función añadida después de que el lenguaje ya estaba creado, y en el uso real también suele estorbar como algo incorporado tarde
- Es difícil entender y resolver los errores, y cuando se busca una solución también hay que filtrar entre varios runtimes y las distintas formas de async que usa cada uno
- Algunas bibliotecas async quizá no puedan usarse fuera de un runtime async específico
- Desde la perspectiva de alguien con 20 años usando JavaScript y experiencia con Go, el async de Rust ha sido la mayor fuente de frustración y fricción
- No es un problema imposible de superar, pero siempre hay que estar preparado para que aparezcan problemas de async en cualquier momento
- En otros lenguajes, async suele funcionar de manera tan natural que casi ni se nota
El refactoring puede convertirse en trabajo pesado
- El rico sistema de tipos de Rust es a la vez una ventaja y una desventaja
- Pensar con tipos de Rust es bueno, pero gestionar los tipos de Rust puede volverse una pesadilla
- Los datos y las firmas de funciones pueden incluir generic type, generic lifetime y trait constraint
- Incluso los propios constraints pueden volver a tener generic type y lifetime, así que a veces hay más type constraints que código real
- Ejemplo: rxRust observable.rs
- Hay que definir los generics en cada
impl, así que ya desde la primera escritura resulta engorroso, y en el refactoring un cambio pequeño puede crecer en cascada- Ejemplo: Wasmtime Cranelift iter.rs
- Cuando hay que repetir la misma lista de constraints o generics en varios lugares, no existe una forma a nivel de lenguaje o herramientas para convertirla en alias o referenciar una definición central, así que queda la carga de la duplicación
Veredicto final: poderoso, pero costoso
- Rust es lo bastante versátil como para escribir código a nivel de sistemas, apps CLI, servidores web y clientes web en un mismo lenguaje
- Con WebAssembly, el mismo binario puede ejecutar un LLM en el navegador y en la línea de comandos
- Los programas en Rust pueden ser muy sólidos, y cuando uno experimenta los problemas que Rust evita, se vuelve difícil regresar a otros lenguajes
- Cuando volvió por un tiempo a Go, la velocidad de desarrollo volvió a ser atractiva, pero tras sufrir un panic en tiempo de ejecución, esa ventaja perdió fuerza
- Rust sí tiene desventajas claras
- Es difícil contratar
- El aprendizaje es lento
- Es demasiado rígido para la iteración rápida
- Especialmente en código async, es difícil rastrear problemas de memoria y rendimiento
- No todas las bibliotecas son lo bastante buenas para código seguro
- Las herramientas de desarrollo todavía tienen mucho margen de mejora
- Aunque con un equipo pequeño lograron cosas sorprendentes, también hubo grandes obstáculos, y como además existían razones técnicas por las que Rust encajaba mejor, aún considera pronto decidir si Rust realmente valió la pena para Wick
- Si se necesita iterar rápido, Rust probablemente no sea una buena opción
- Si el alcance ya se conoce o se puede asumir un costo inicial más alto, vale la pena considerar Rust seriamente
- A medida que la perspectiva de WebAssembly gana fuerza mes a mes, la posibilidad de reutilizar software sólido escrito una sola vez en muchos lugares se siente cada vez más cercana a la realidad
1 comentarios
Comentarios de Hacker News
He usado mucho Rust, pero incluso después de varios años sigo sintiendo que la productividad es baja
Últimamente uso mucho Zig, y siento que soy como 10 veces más productivo porque puedo concentrarme solo en el código que quiero escribir y no tengo que pensar qué herramienta o biblioteca debería usar
Entiendo que Rust ofrece seguridad de memoria y que eso importa, pero su usabilidad es realmente mala. Cada vez que uso Rust siento que estoy restringido, y como siempre tengo que buscar bibliotecas o investigar cómo hacer algo, no puedo simplemente “ponerme a escribir código”
El sistema de tipos también puede crecer hasta volverse inmanejable, y muchas veces es difícil saber qué métodos se pueden invocar realmente en cierta estructura. Rust es una herramienta excelente y resuelve muchos problemas, pero no creo que sea un buen lenguaje de propósito general
Incluso hablando desde alguien que ha usado Python por casi 20 años, ahora trabajo en Rust tan rápido como en Python
Para mí, Rust falla por completo en encontrar el equilibrio adecuado. Es demasiado exigente con detalles de bajo nivel para escribir aplicaciones de alto nivel, y demasiado complejo para trabajar en embebidos o sistemas operativos
Para lo primero elegiría C++, Java, Haskell, OCaml, o incluso Go mezclado con un poco de C; para lo segundo, C usado casi como macroensamblador encaja mucho mejor
Sigo sintiendo que la idea original de Graydon Hoare —algo más del lado de OCaml/SML con tipos lineales, recolección de basura, asignación en stack, green threads y CPS— habría sido un lenguaje mucho mejor
En C, un pequeño error suele terminar en comportamiento indefinido y dolores de cabeza, pero en Rust eso no pasa, y para mí cambió por completo las reglas del juego
También me pregunto si con eso quieres decir que en Zig no hace falta buscar bibliotecas ni averiguar cómo hacer las cosas
Rust te obliga a decidir de antemano adónde va cada bit y byte, en qué hilo se usa y bajo qué forma de mutación se va a manejar. A menos que estés haciendo parsers o cosas a nivel de microcontrolador, ese proceso se siente tedioso
Me gusta primero hacer que algo funcione y después decidir cuál es la mejor estructura de API, y Rust choca con ese proceso
Aunque el sistema de tipos de Rust es más potente, con Swift también puedes obtener el 90% del rendimiento y el flujo se siente mucho más natural
La mayor crítica podría ser que crates.io no tiene espacios de nombres
Cualquiera puede apartar nombres de paquetes globales y genéricos, y a menos que evites el repositorio de crates.io, la mayoría tiene que aguantarse con eso. Pero algunos de esos paquetes con nombres genéricos ya apartados ni siquiera son los mejores para usar en la práctica
Tal vez fue una reacción contra la notación de DNS invertido al estilo Java por ser larga y molesta, pero algo como poner un espacio de nombres de usuario/grupo delante del nombre del paquete, al estilo GitHub, habría sido un buen punto medio
Envié ese análisis al equipo de crates.io y también señalé que existe una política contra la automatización, pero respondieron que no era evidencia suficiente de acaparamiento de nombres
El problema con crates.io es que, aunque haya políticas claras, no las hacen cumplir. Por eso, todos los nombres cortos y fáciles de recordar ya están tomados, y no hay forma de recuperarlos
No recuerdo muchos gestores de paquetes no relacionados con Java en 2014~2015, cuando apareció Rust, que no usaran un único espacio de nombres global: NPM, PyPI, RubyGems, Hex de Elixir, Cabal de Haskell, etc.
Algunos intentaron corregir eso después, pero en ese entonces así era como funcionaban los gestores de paquetes
Los peores sistemas de gestión de dependencias de otros lenguajes que vinieron después casi no aprendieron nada de esos casos anteriores
Además tiene la ventaja de que ya no hace falta una base de datos global de paquetes a nivel de lenguaje. Subes un paquete a example.com/your-thing y queda liberado de inmediato
Claro, si quieres, cachés y motores de búsqueda se pueden ofrecer por separado
Es como eso de que http-server es malo y en cambio deberías usar MuffinTop, pero simplemente tienes que saberlo
La idea de un nombre de paquete oficial es interesante, pero si con el tiempo cambia el código detrás del alias, en realidad es muy probable que termine causando confusión
Al final, parece que esto seguirá siendo parte del proceso de volverse experto en un ecosistema
Si creas
.cargo/config.tomlen la raíz del workspace, se aplica a todos los crates, así que puedes configurar lints globales de ClippyDentro del archivo, bajo
[build], basta con poner algo comorustflags = ["-Wclippy::lint_name_to_warn", "-Dclippy::lint_name_to_deny"]Eso sí,
rustflagsno se agrega sino que sobrescribe, así que si hay otra fuente como la variable de entornoRUSTFLAGS, esa configuración quedará reemplazadalib.rsomain.rs. Es simpleEstoy aprendiendo Rust porque parece claro que va a volverse importante profesionalmente. De verdad quiero que me guste y le veo ventajas, pero entra en la categoría de los lenguajes más desagradables que he usado hasta ahora
Seguí esperando que, al ganar soltura, se me pasara ese rechazo, pero mientras más avanzo por la curva de aprendizaje, no es que le agarre cariño
Está bien. Seguro no será el único lenguaje que maneje bien y aun así no me guste. Solo que hay tanta gente diciendo que ama Rust que pensé que yo también lo disfrutaría
También da la impresión de que el compilador de Rust ha mejorado para aceptar casos válidos más amplios
La clave para entender el borrow checker fue entender el modelo de memoria subyacente. El modelo de memoria de Rust es igual al de C, salvo por extensiones para abstracciones como genéricos
Las reglas del borrow checker al principio parecen arbitrarias, pero están profundamente conectadas con ese modelo de memoria. Cuando caigo en el borrow checker sin querer, ahí es donde realmente aporta valor, porque suele ser un bug causado por un descuido
Lo aterrador es que, en un lenguaje como C o C++, ese código simplemente se aceptaría y seguiría adelante
El sistema de tipos estricto de Rust y el borrow checker te empujan suavemente a estructurar bien el código, y estoy convencido de que han mejorado cómo diseño código en todos los lenguajes que uso
Es una función de usabilidad muy básica para programadores, compartida por la mayoría de los lenguajes populares, e incluso C++ tiene argumentos predeterminados desde hace muchísimo
Como exprogramador de C/C++ que llamó a pthread casi todas las semanas durante 10 años, ahora uso Rust asíncrono para todo
No entiendo por qué lo asíncrono recibe tanto odio. En mi opinión, todos deberían usar async para todo. Incluso para tareas “simples” que en apariencia son de un solo hilo
Si el caso de uso del autor hubiera sido Wasm, seguramente tendría otra perspectiva
Los trabajos que usan buffers grandes o los reutilizan para evitar el costo de asignación muchas veces también se benefician del viejo estilo de pool de hilos. Se puede mitigar algo con bump allocators o allocators fragmentados, pero cuando estás limitado por CPU en bucles ajustados que pueden vectorizarse, un pool de hilos suele funcionar mejor
Async es una buena herramienta, pero no existe solo el contexto óptimo para ella
Go, C# y TypeScript no tienen esa barrera
Por ejemplo, parece que ni siquiera puedes usar
awaitdentro de la funciónmainsin un decorador importado de TokioEnginede rhai, no sonSend, y estoy tratando de usarlos dentro de un closure asyncGPT-4 sugirió crear un
tokio Runtimedentro de un hilo y usarblock_on(). Voy a intentarlo mañana. Este es mi primer proyecto serio en RustTambién me da curiosidad si cuando dices “ahora uso Rust asíncrono para todo” quieres decir “uso Tokio para todo”
Programar en Rust de verdad no se parece a una relación abusiva. El compilador intenta ayudarte al máximo y, en particular, los mensajes de error de rustc están entre los mejores del mundo
Lo que se parece más a una relación abusiva no es el compilador de Rust, sino el sistema operativo. Lo mismo con el hardware: también hay que ejecutar correctamente el ensamblador, así que visto así podría llamarse una relación abusiva
Los mensajes de error de Rust están en otra liga, y no hay ningún compilador que se le acerque
Además, hace poco usé LaTeX y los mensajes de error fueron horribles. Fue una pesadilla tener que inspeccionarlos para averiguar qué estaba mal
Futureya no esSendniSyncToda la consola queda inundada de errores, y el error de sintaxis real queda enterrado por ahí en medio
Mi principal queja es que todavía no entiendo por completo los lifetimes, y el compilador tampoco puede ayudarte siempre. Se entiende, porque el compilador tiene que ser conservador
En las pruebas de C++ se suele decir: “si compila, probablemente está bien”
Si crees que Rust maneja tantos errores que los casos de prueba comunes se vuelven inútiles, yo lo vería como una señal de que en otros lenguajes no se estaba probando lo correcto
Lo que hay que probar no son los problemas del lenguaje en sí, sino la lógica de negocio
Si al ver un test piensas “en JavaScript lo probaría, pero en Rust no hace falta”, entonces simplemente hay que borrar ese test
No existe esa separación entre “lógica de negocio vs. problemas del lenguaje”, porque el lenguaje es la base sobre la que se sostiene la lógica de negocio
Si no pruebas los modos de fallo, no sé cuál sería el sentido de hacer pruebas
A diferencia de la mayoría de los lenguajes conocidos, C++ tiene IFNDR, y en broma se le llama un falso positivo a la pregunta “¿esto es un programa en C++?”
A un compilador de C++ conforme al estándar se le prohíbe avisarte en algunos casos donde sospecha que el código que escribiste no tiene sentido; simplemente debe seguir adelante y emitir algo
Ese algo puede ser un ejecutable funcional, o un ejecutable que provoque un desastre total todos los viernes. No hay forma de saberlo
El estándar ISO sí identifica estos casos, pero de forma tan vaga que es difícil delimitar exactamente qué entra ahí, y mi impresión es que hoy en día la mayoría del software no trivial en C++ probablemente sea IFNDR. Mejor rechazar todo el lenguaje
Rust parece haber roto por fin la idea de que el programador debe controlar por completo y ser consciente de todo lo que hace el compilador
En realidad eso ya no era cierto desde hace décadas, y los compiladores eran casi magia. Rust revirtió bastante esa tendencia con los préstamos, haciendo que la gente se sintiera cómoda con que el compilador sabe más que uno mismo
Ojalá se avanzara aún más. No debería ser necesario iterar explícitamente una colección de principio a fin salvo que el algoritmo lo requiera. Varias tareas deberían paralelizarse implícitamente. Ojalá existiera un Bash al estilo Rust
Rust es bastante transparente respecto de lo que hace, y es muy conservador con la magia del compilador. El lenguaje no hace asignaciones en heap, no hace conteo de referencias y no tiene conversiones implícitas de tipos numéricos
No copia tipos a menos que se haya declarado implícitamente que se pueden copiar, e incluso eso solo es legal para tipos que pueden copiarse con un simple
memcpysuperficialRust usa abstracciones de costo cero por todas partes, así que suele ser predecible a qué código va a compilar, y normalmente es simple. También se conoce bien la disposición por defecto de los tipos estándar, así que se sabe que recorrer un
Vecva a compilar a un bucle que incrementa punteros, y no hay paralelismo implícitoDescribir los préstamos como “el compilador sabe más que el programador” es raro. Los préstamos se parecen más a la comprobación de tipos. Si declaras algo como temporal y luego intentas usarlo como si viviera más tiempo, obtendrás un error
Es lo mismo que declarar que una función devuelve una estructura
Fooy luego devolverBar. Si el compilador “sabe más”, es simplemente porque escribiste un bugLos préstamos también se compilan a uso directo de punteros sin recolección de basura, y en estructuras y funciones con ABI de C se garantiza literalmente que son iguales a punteros de C. Si crees que sabes más que el compilador, incluso puedes saltarte las vidas útiles con
unsafeiter()Aun así, no estoy de acuerdo con que eso deba ser implícito
En algunos sentidos Rust le da al programador más control que C. Por ejemplo, Rust soporta ensamblador en línea como parte estándar, mientras que el ensamblador en línea de C depende de extensiones específicas del proveedor
Eso sí, los valores por defecto convenientes son muy distintos. En Rust, los casts inseguros de tipos requieren mucho procedimiento y cuidado, y hay que seguir más reglas que en C
En particular, las referencias de Rust en la práctica actúan todas como
restrict, y es muy fácil arruinar eso al hacer castsunsafedesde punteros sin procesar a referencias seguras. Por eso hay un fuerte incentivo para no escribir ese tipo de código si tienes otra opciónMás bien quiero un control todavía más explícito, y me gustaría reducir esa carga con un sistema de tipos más expresivo. Idealmente, me gustaría que el sistema de tipos de Rust se pareciera más a una variante de Prolog
Aprender cuándo invertir en restricciones de tipos y cuándo no hacerlo es una lección importante
No es un problema exclusivo de Rust, aunque la forma de expresarlo puede ser un poco distinta
He trabajado con C++ sobretipado y con Java excesivamente abstraído y tipado, y ambos tienen el mismo tipo de problemas al refactorizar
Por otro lado, también he visto mucho Go con pocos tipos y poca documentación, donde ciertos valores quedan dispersos por todas partes y se convierten en minas terrestres en tiempo de ejecución, y refactorizar puede volverse realmente muy difícil
Al principio se siente que avanzas más rápido, pero normalmente terminas entregando bugs a los usuarios
No hay una respuesta mágica en esta compensación. Rust ofrece un rango bastante amplio de opciones en este eje
La frase “Rust te grita todo el día, todos los días, por cosas que en tu vida anterior habrías considerado completamente normales” también aplica más o menos a un buen compilador de C si activas todos los flags
Me gustan los lenguajes y compiladores donde puedes apagar opcionalmente esos gritos y te dejan escribir código malo a propósito. Muchas veces es mejor tener código malo que funciona y se puede escribir rápido que código perfecto pero que toma una eternidad
Puedes hacer una prueba de concepto mala pero funcional, y luego corregirla para que sea menos mala
El punto de que “atrae bien a nuevos usuarios, pero las bibliotecas o herramientas no mejoran de forma dramática, y solo aparecen forks de una sola vez para resolver casos de uso específicos” no tiene que ver con la edad
Es difícil atraer desarrolladores clave, y hace falta mucho esfuerzo para volverlo atractivo. Además, las convenciones culturales las definen los primeros adoptantes, y no tener convenciones muchas veces es tan dañino como tener malas convenciones
Si miras Python, por la forma relajada en que se abordaron los entornos de desarrollo y ejecución, terminaron compitiendo como 50 maneras distintas de desarrollar o ejecutar programas en Python
PyPI, el repositorio de paquetes más usado, fue un desastre durante años; pocos construyen sobre paquetes existentes, los nombres parecen generados por un generador aleatorio de palabras, el ecosistema tiene mucho malware, y ni siquiera puedes buscar paquetes desde la línea de comandos
No es culpa del lenguaje, sino de una comunidad y un equipo central que actuaron como espectadores. La cultura importa más que la tecnología que está en el centro
No lo digo para criticar solo a Python; simplemente conozco mejor ese problema. C lleva medio siglo existiendo, pero su comunidad tampoco ha logrado ordenar bien ni la mitad de las soluciones que los lenguajes más modernos ya prepararon
Rust también permite eso. Solo hay que marcarlo todo como unsafe
Es poco probable que Linux y Windows hayan empezado a portar componentes a Rust por la razón de que “no hay bibliotecas mejoradas”
Me atrevería a decir que eso se parece mucho a TypeScript
Me refiero a esas situaciones en las que el compilador te grita que lo arregles, pero antes de pulirlo por completo solo quieres probar una idea