10 puntos por GN⁺ 2025-08-28 | 3 comentarios | Compartir por WhatsApp
  • Rust permite hacer refactorización con confianza incluso en bases de código grandes gracias a sus sólidas garantías de seguridad, lo que mejora la productividad y la mantenibilidad
  • El compilador puede detectar de antemano bugs relacionados con la planificación asíncrona, reforzando la estabilidad al evitar comportamiento indefinido
  • Lenguajes como TypeScript suelen encontrar bugs asíncronos en producción con frecuencia debido a su sistema de tipos más flexible
  • El sistema de tipos de Rust deja claro el impacto de los cambios en el código, lo que aumenta la confianza y la disposición a experimentar en proyectos complejos
  • A diferencia de Rust, Zig tiene verificaciones más laxas en el manejo de errores, por lo que puede pasar por alto bugs causados por errores tipográficos y resultar menos confiable

Resumen y contexto

  • El backend de Lubeno está escrito 100% en Rust, y la base de código ha crecido hasta un punto en el que ya es difícil abarcarla completamente de memoria
    • En proyectos grandes, por lo general es difícil verificar los efectos secundarios de los cambios, lo que provoca una caída de productividad
  • Las garantías de seguridad de Rust dejan claro el impacto de los cambios en el código, reduciendo el miedo a la refactorización
    • Esto contribuye a una mejor mantenibilidad y a una mayor productividad a largo plazo
  • Este texto comienza con un caso en el que el compilador de Rust detectó un bug asíncrono y explora las ventajas de productividad de Rust

Un caso de las garantías de seguridad de Rust

  • Situación del problema: se encapsuló una estructura con un mutex para permitir acceso concurrente, y luego se ejecutó una operación asíncrona después de adquirir el bloqueo
    let lock = mutex.lock();  
    db.insert_commit(commit).await;  
    
  • Descubrimiento del problema: rust-analyzer no mostró ningún error, pero apareció un error de compilación en el archivo de definición de rutas
    .route("/api/git/post-receive", post(git::post_receive))  
                                         ^^^^^^^^^^^^^^^^^  
    error: future cannot be sent between threads safely  
    
  • Análisis de la causa:
    • El framework web crea una tarea asíncrona por cada conexión HTTP, y el planificador de tareas mueve esas tareas entre hilos
    • El mutex requiere que el bloqueo se libere en el mismo hilo; si el hilo cambia en el punto de .await, podría producirse comportamiento indefinido
    • El compilador de Rust rastrea la vida útil del bloqueo y detecta la posibilidad de que se libere en otro hilo
  • Solución: liberar el bloqueo antes de .await
  • Importancia: Rust evita en tiempo de compilación bugs asíncronos que son difíciles de reproducir en el entorno de desarrollo

Caso comparativo con TypeScript

  • Situación del problema: aparece un bug de redirección asíncrona en código TypeScript
    if (redirect) {  
        window.location.href = redirect;  
    }  
    let content = await response.json();  
    if (content.onboardingDone) {  
        window.location.href = "/dashboard";  
    } else {  
        window.location.href = "/onboarding";  
    }  
    
  • Causa del problema:
    • window.location.href no redirige de inmediato, sino que programa la redirección, por lo que la ejecución del código continúa
    • Debido a una condición de carrera, ocurre una redirección no intencional
  • Solución: agregar return al bloque if
    if (redirect) {  
        window.location.href = redirect;  
        return;  
    }  
    
  • Limitación: TypeScript no tiene rastreo de lifetimes ni reglas de préstamo, así que no puede detectar este tipo de bug en tiempo de compilación
    • Se descubre en producción y puede tomar mucho tiempo depurarlo

Ventajas de Rust para la refactorización

  • En desarrollo web, Python, Ruby y JavaScript/Node.js ofrecen alta productividad inicial, pero cuando la base de código crece, su acoplamiento flexible dificulta hacer cambios
    • Después de un cambio aparecen errores inesperados, lo que reduce la disposición a modificar el código
  • En Rust, el sistema de tipos deja claro el impacto de los cambios, reduciendo el miedo a la refactorización
    • Ejemplo: advertencias del tipo “este cambio podría afectar otras partes” permiten prevenir problemas de antemano
  • Incluso a medida que crece la base de código, la productividad aumenta, porque se puede reutilizar código existente y mantener la estabilidad al hacer cambios

Comparación con las pruebas

  • Las pruebas son útiles para evitar regresiones al refactorizar, pero como el compilador no las obliga, es posible omitirlas
    • Escribir pruebas implica decidir el nivel de abstracción, comportamiento vs. detalles de implementación, y si realmente previenen errores, lo que genera una carga mental importante
  • Rust hace que el compilador bloquee de antemano errores comunes, reduciendo la carga de decisión que implican las pruebas
    • Las propiedades que no pueden verificarse con el sistema de tipos se complementan con pruebas

Comparación con Zig

  • Zig es un lenguaje de programación de sistemas similar a Rust, pero más laxo en el manejo de errores
    • Ejemplo: código de manejo de errores
      const FileError = error{ AccessDenied };  
      fn doSomethingThatFails() FileError!void {  
          return FileError.AccessDenied;  
      }  
      pub fn main() !void {  
          doSomethingThatFails() catch |err| {  
              if (err == error.AccessDenid) {  
                  std.debug.print("Access was denied!\n", .{});  
              }  
          };  
      }  
      
    • Debido al error tipográfico AccessDenid, se produce un bug, pero el compilador de Zig lo trata como un número y la compilación se completa con éxito
  • Al usar una sentencia switch se detecta el error tipográfico, pero en una sentencia if se ignora, lo que genera problemas de confiabilidad
  • Rust evita este tipo de debilidades de diseño y verifica estrictamente errores tipográficos y fallas lógicas

Implicaciones

  • Rust mejora la productividad y la estabilidad en proyectos grandes gracias a sus garantías de seguridad y a su estricto sistema de tipos
  • Puede detectar en tiempo de compilación problemas complejos como bugs asíncronos, reduciendo los costos de mantenimiento
  • Los casos de TypeScript y Zig muestran los riesgos de las verificaciones laxas y resaltan el valor del compilador estricto de Rust
  • Rust se consolida como una herramienta poderosa en desarrollo web no solo por la productividad inicial, sino también para la gestión de bases de código a largo plazo

3 comentarios

 
taptaps 2025-08-30

Cada vez que veo que dicen "¡esto es lo mejor, es un lenguaje poderoso!!", no puedo evitar pensar:
¿será que en realidad no hay tantos desarrolladores de Rust como uno imaginaría, y por eso andan tratando de convencer a la gente de usar Rust?

 
colus001 2025-08-29

¿Soy el único al que los artículos recomendados sobre Rust le suenan a algo como el gourmet de Sikgaek diciendo "¡Prúebalo! ¡Prúebalo!"?

 
GN⁺ 2025-08-28
Comentarios en Hacker News
  • El año pasado porté un driver de red virtio-host escrito en Rust. Cambié el backend, el mecanismo de interrupciones y lo pasé de biblioteca a proceso independiente. Era un programa complejo que manejaba mapeo de memoria, interrupciones de VM, sockets de red y multihilo. Casi no tenía experiencia con Rust y poca con virtio, pero para cuando el proyecto compiló, funcionaba perfectamente. Salvo un bug relacionado con Drop, fue fácil de corregir. Creo que ayudó mucho que las bibliotecas de Rust estén diseñadas de forma que sea difícil usarlas mal

    • Llevo mucho tiempo desarrollando en Rust, y la mayor parte del tiempo, si compila, el código funciona bien. A veces salen deadlocks o bugs relacionados con el orden, pero en general que compile significa que una gran parte del proyecto ya está funcionando correctamente
  • Me parece que Rust es excelente. Pero no estoy de acuerdo con la idea de que el bug de asignación de href sea culpa de TypeScript. El núcleo del problema es que, aunque configures href, la navegación de página no ocurre de inmediato, sino que se procesa después. En Rust podría pasar exactamente lo mismo. Si en Rust hubiera una función set_href y ese comportamiento se procesara más tarde, sería posible escribir algo así:

    set_href('/foo')

    if (some_condition) { set_href('/bar') }

    Creo que en Rust no se diseñaría así. Que un setter dispare una acción no es buen diseño de biblioteca, y que al asignar href no ocurra la navegación al mismo tiempo es raro. En la biblioteca estándar de Rust no habría una implementación tan tonta. No es un tema de Rust vs TypeScript, sino de la biblioteca estándar de Rust frente a la Web Platform API. Sí estoy de acuerdo en que Rust no ofrecería una experiencia de usuario así

    • Hablando formalmente, no es deseable diseñar algo para que una acción ocurra de inmediato desde un setter. También habría que cambiar el nombre a algo como navigate_to(href). En el entorno del navegador, todo el código JS funciona mediante callbacks y está controlado por el event loop, así que también es natural que no se ejecute inmediatamente

    • El ejemplo de Rust es interesante, pero solo con el ejemplo de TypeScript no se puede saber si TS sirve para proyectos grandes. En Ruby me da inseguridad tener que atrapar bugs en runtime con frecuencia, pero al final funciona bien antes de hacer commit, y leer y modificar el código es fácil y satisfactorio. El tema de la navegación es un problema de JavaScript, y TS lo hereda. Surge porque JS permite modificar propiedades libremente. Pero como la página tampoco desaparece de inmediato, una vez que entiendes el comportamiento, resulta razonable

    • Técnicamente, en Rust se podría insinuar el significado con claridad según si set_href devuelve () o !. Pero incluso así, en una redirección condicional seguiría siendo difícil detectar el mal uso

    • Lo que quería señalar es que, con el modelo de ownership de Rust, se podría diseñar una API donde window.set_href('/foo') tome posesión de window, de modo que no sea posible llamarlo dos veces. TypeScript ni siquiera tiene el concepto de rastreo de lifetimes, así que eso no puede existir ahí. Y como la API de JS ya existe, tampoco hay forma de introducir un sistema de ownership desde TypeScript. Quería mostrarlo como un caso donde varias funciones de Rust se combinan para ofrecer garantías más fuertes

    • Suena a que tu base para decir que Rust es mejor termina siendo “porque los programadores de Rust son mejores”. No creo que los programadores de Rust hagan ese tipo de argumento circular

  • Después de la asignación, el código sigue ejecutándose a menos que se haga un retorno anticipado de forma explícita. En serio, no entiendo por qué alguien pensaría que asignar un valor va a detener la ejecución del script. Puede que al ejemplo de TS le falte contexto, pero es raro usarlo como ejemplo de “data race”

    • Asignar un valor a window.location.href tiene el efecto secundario de hacer que el navegador navegue a ese enlace. Ese comportamiento es inesperado, y como una simple asignación carga una página nueva, se siente un poco como execve, así que no es raro pensar que la ejecución de JS se detendría de inmediato. No deberías depender de esa suposición al programar, pero como el comportamiento en sí es extraño, entiendo que pueda confundir

    • Lo piense uno así o no, este tipo de bug queda claro de corregir una vez que alguien te lo señala. El punto principal que intentaba hacer el autor es que bugs así, que TS no puede detectar, pueden ser realmente difíciles de encontrar y tomar mucho tiempo

    • exit(), execve(), etc. sí detienen la ejecución de inmediato, así que uno podría pensar que una redirección se comporta igual

    • Es raro cuestionarlo solo por haber compartido su experiencia

    • Esa asignación tiene un efecto secundario grande: hace que abandones la página. Tampoco me parece descabellado pensar en eso como una acción asíncrona que ocurre de inmediato. Yo también he hecho esa suposición antes

  • Es la historia de un desarrollador que se dio cuenta de que un sistema de tipos estático es útil. Siempre me da risa cuando veo textos así

    • Lo que quería mostrar en mi blog es que el rastreo de lifetimes y el sistema de traits de Rust pueden detectar problemas mucho más complejos que una simple incompatibilidad de tipos. TypeScript también es un lenguaje con tipos estáticos, pero no ofrece garantías al nivel de Rust
  • ¿No será que la mayoría de las ventajas viene simplemente de usar tipos estáticos, es decir, un lenguaje compilado? Lo mismo pasa con Java, Go y C++. TypeScript tiene sus trucos: compila a JS y hereda sus problemas, pero aun así sirve. Rust tiene un sistema de tipos más estricto y por eso puede hacer chequeos adicionales en tiempo de compilación, aunque a cambio me parece más difícil de aprender y también más difícil de leer

    • Hasta cierto punto estoy de acuerdo, pero Rust tiene más dimensiones en su sistema de tipos: ownership, acceso compartido/exclusivo, thread safety, sum types, etc. Gracias al sistema de ownership/borrowing, queda claro si al pasar un argumento se entrega una vista temporal o si se transfiere completamente. Eso ayuda mucho en programas grandes o al usar bibliotecas externas. Por ejemplo, en Go el tipo slice no deja claro en runtime qué operaciones están permitidas, y tampoco está muy claro cómo prestarlo de forma de solo lectura. Rust además puede garantizar thread safety a nivel del sistema de tipos, lo que permite bloquear en tiempo de compilación data races que en otros lenguajes serían difíciles de encontrar en runtime

    • Ver todos los lenguajes con tipos estáticos como si fueran uno solo probablemente significa que todavía no has sentido el verdadero poder de los tipos union (sum) y del pattern matching. Una vez que te acostumbras a los union types, los lenguajes estáticos tradicionales ya no te satisfacen igual

    • Una gran ventaja son los traits/impl traits. En Rust puedes agregar traits a cualquier tipo después, algo parecido a los Extension Methods de C#. En la mayoría de los lenguajes, el tipo queda completamente definido en la biblioteca donde se creó, pero en Rust puedes ir acumulando funcionalidad sobre tipos simples de forma progresiva. Ese carácter de late-bound introduce un elemento de dinamismo en el sistema de tipos. Dicho de forma un poco extrema, el verdadero superpoder de Rust no es el borrow checker, sino la apertura y flexibilidad de su sistema de tipos. No necesitas diseñarlo todo desde el principio; puedes extenderlo gradualmente

    • No todos los lenguajes con tipos estáticos producen el mismo efecto. Java al final depende de Object y de casts en runtime. Go no tiene enum. C++ agregó algo como variant, pero para usarlo con seguridad hace falta manejo manual tipo try/except, así que estructuralmente es incómodo

    • Dices que Rust es difícil de aprender, pero la verdad es que, si realmente lo aprendes bien, no es difícil. Al principio de la programación es importante poder escribir algo rápido y lograr que medio funcione, y Rust es un lenguaje poco amable con esa forma de trabajar. No lo recomendaría como lenguaje de entrada, pero no me parece difícil de leer

  • Gracias a la fuerte seguridad de Rust, aumenta mucho la confianza al tocar una base de código. Con esa confianza ya no da miedo refactorizar partes críticas, y como resultado mejoran mucho la productividad y la mantenibilidad. Pero para eso existen las pruebas. Sin tests, un compilador estricto ayuda bastante, pero si escribes buenas pruebas puedes refactorizar con confianza en cualquier lenguaje

    • Es mejor que el compilador demuestre estáticamente todo lo que sea posible. Las pruebas son óptimas solo para situaciones en las que es difícil obtener garantías estáticas. El ideal absoluto sería la verificación formal; en la práctica es muy difícil, así que no es algo generalizable, pero como principio es correcto

    • Tanto unas buenas pruebas como un sistema de tipos bien aprovechado son efectivos para encontrar bugs. Pero escribir pruebas a veces me recuerda al cómic de xkcd de “Standards”. Es como intentar corregir bugs escribiendo más código. Aun así, el mantenimiento del sistema de tipos lo hace el diseñador del lenguaje, así que no hay que administrarlo proyecto por proyecto

    • Cada vez que refactorizas código también tienes que refactorizar las pruebas, así que el trabajo se duplica

  • Creo que el sistema de tipos de Rust o F# brilla más al refactorizar código. La expresión “refactorización sin miedo” le queda perfecta

    • La desventaja es que Rust no tolera código incompleto, así que durante una refactorización no puedes tener “código parcialmente funcional”. O lo terminas por completo, o no haces nada, así que escribir código experimental puede ser incómodo. Pero esa misma rigurosidad termina siendo uno de los factores que llevan a buen código
  • El ejemplo de Zig me impactó. Se ve demasiado inestable y no entiendo cómo alguien podría pensar que ese diseño es bueno

    • Creo que eso probablemente es un bug. Pero en un lenguaje centrado en su creador como Zig, para que se corrija un bug también importa que ese creador lo reconozca como bug. Si lo considera intencional, podría seguir así

    • Todos los lenguajes tienen un poco de diseño inestable. Por ejemplo, en Go o Zig siempre tienes que hacer mutex.unlock() explícitamente y no se libera automáticamente al salir del scope. En cambio, con algo como el operador as de Rust, es fácil hacer conversiones entre tipos numéricos, y por eso mismo me he pasado un día entero persiguiendo bugs

    • Al principio no había visto ese error, pero me di cuenta al leer este comentario

    • Me pregunto si un linter podría advertir sobre referencias a errores que no existen dentro del sistema y recomendar usar switch

    • Pensé que los sets de errores se generaban a partir de la firma de la función. Es algo bastante peculiar

  • Me gusta que un sistema de tipos estático fuerte y sólido ofrezca tantas capacidades. Yo también tuve la experiencia de poder hacer refactorizaciones grandes con facilidad en una base de código Haskell (1 millón de SLOC). Incluso sin funciones avanzadas, solo con el sistema de tipos ya fue suficiente

  • Rust detectó correctamente que se estaba manteniendo un lock a través de una frontera de await, pero para saber si realmente es seguro liberar ese lock antes del await hace falta más contexto. Pienso que el lock debería mantenerse hasta que se genere el commit de la transacción, y si se libera antes del await podría haber problemas de concurrencia. No conozco bien Rust async, pero me da la impresión de que después del commit habría que bloquear con join o select

    • Si necesitas mantener un lock durante un await, puedes usar un mutex compatible con async. Los crates futures o tokio implementan este tipo de locks. Se usan sobre todo cuando hay que mantenerlos mucho tiempo o entre await. Son más costosos que un lock normal

    • Si necesitas mantener un lock incluso a través de una frontera de await, puedes usar el mutex async-aware de Tokio. Consulta la documentación de tokio/sync/struct.Mutex