
Aprender un nuevo lenguaje de programación te va a costar más o menos dependiendo de la experiencia actual que tengas con otros. Algunos, se aprenden por acumulación, esto es, sumas sintaxis, sumas librerías, añades trucos, y practicando hasta la extenuación, claro está.
Y luego está Rust, que se aprende por sustitución. No porque sea caprichoso, sino porque te obliga a cambiar el modelo mental con el que llevas años resolviendo problemas, con algunos mecanismos del lenguaje que, reconozco, me han costado bastante digerir, hasta que en algún momento, algo te hace click y comprendes la razón de esos puntos que al principio parecen un tanto esotéricos, sobre todo si cometes el error de compararlos con otros lenguajes.
Si vienes de lenguajes interpretados y de muy alto nivel como Python o Javascript, Rust de va a sonar a chino, literalmente.
Si tu mundo es Java o C#, la curva de aprendizaje será, digamos, media.
Y por último, si conoces C++, tu aprendizaje de Rust será algo natural, aunque también te requerirá cierto esfuerzo.
Al inicio de mi carrera profesional (hace más años de lo que recuerdo), comencé programando en C++, de modo que cuando me acerqué a Rust hace unos años, de alguna manera pensé que me resultaría ameno y hasta atractivo, pero no fue así: comencé leyendo el libro clásico Programming Rust mas toda la documentación de la web del lenguaje (muy buena, por cierto), pero me quedé bloqueado en varias ocasiones, después de optar a un proyecto en WebAssembly con Rust (que al final no salió).
No obstante, siempre volvía a Rust y a su ecosistema, más que nada por curiosidad y por la escalabilidad en mis escarceos en infraestructuras HPC (high performance computing), en donde ahorrar hasta el último byte y ciclo de cpu son importantes.
Y de ahí este artículo, cuya intención es principalmente insistir en los conceptos que debes asumir y comprender muy bien desde el principio de Rust para no estrellarte, hasta que comienzas a ver el bosque completo.
Hoy día siento cierta fluidez con Rust, es más, diría que hasta entusiasmo. Rust no te pregunta “¿cómo quieres programar?”. Rust te pregunta “¿qué garantías estás dispuesto a declarar?”. Y en esta pregunta, que tiene cierto tono filosófico, está la razón por la que cuesta tanto al principio… y por la que, cuando por fin te iluminas, ya no miras igual ni el lenguaje ni el resto de su ecosistema.
Siempre he odiado las comparaciones infantiles entre lenguajes y entornos: cada uno tiene su sitio y su nicho, y nuestra habilidad es saber elegir qué entornos, tecnologías, etc. son las más adecuadas según cada proyecto (y su ciclo de vida futuro).
Este artículo, a modo de brújula, es una forma de nombrar lo que suele doler en Rust cuando vienes de otros lenguajes y que a mí me hubiese gustado encontrar al comienzo.
En cierto modo, Rust no es una nueva sintaxis, sino una especie de nuevo pacto al programar, muy diferente de otros lenguajes aunque, obviamente, tenga sus connotaciones similares con C++.
1) Rust no es C++, ni Java, ni Python (aunque tenga ciertos parecidos a elementos concretos de todos ellos)
Es tentador clasificar Rust por proximidad o similitud a otros lenguajes:
- “Se parece a C++ porque compila y es rápido”.
- “Se parece a Java/C# porque tiene traits que recuerdan a interfaces”.
- “Se parece a Python en la ergonomía moderna y el tooling”.
Y sin embargo, si lo abordas desde esas comparaciones, Rust se te queda como una maqueta: se parece por fuera, pero por dentro no encaja.
En C++ el poder se negocia con convenciones, disciplina (no te olvides de los "delete") y una cultura de haz lo correcto, pero nada, salvo tu buen hacer, te obliga a hacer lo correcto. En Rust, como veremos, esto es diferent.
En Java/C# la memoria y la vida de los objetos se externaliza a un runtime y el poderoso recolector de memoria que tango te aligera programar y despreocuparte del ciclo de vida de variables y recursos. En Python, por su parte, el coste está en la ejecución, no en la compilación, te centras en lo funcional y el lenguaje mismo te da ciertas garantías, pero todo tiene un coste, claro.
Rust, en cambio, te da la primera en la frente al mover una parte importante de la complejidad de una aplicación al momento de compilarla. Sí, has leído bien: me costó mucho captar esta idea y su razón.
En otros lenguajes, muchos asuntos internos de una aplicación, de bajo nivel, te lo dan todo hecho, pero Rust es mucho más explícito, y esto duele al principio (y luego te enamoras precisamente de esta cualidad que abordas y fluye de forma natural y productiva).
2) El uso de la memoria no es un detalle interno: es una parte del diseño
La intención principal de Rust es evitar bugs antes de que aparezcan en ejecución (lagunas de memoria, data races, punteros tontos, accesos no permitidos, etc.), y para eso, para muchos tipos de errores, tienes que preocuparte explícitamente de cómo tu aplicación usa internamente la memoria. Pero tranquilo, que, en realidad, esto es más fácil de lo que suena.
En muchos lenguajes, la gestión de la memoria ni aparece por ningún lado: declaras una variable o instancias un objeto, lo usas, y luego te olvidas, y un recolector de basura o garbage collector (que ni te enteras de que existe), hace su trabajo. Pero para ganar esta fluidez a la hora de escribir código, hay que pagar un precio, bien en la forma de uso ineficiente de la memoria y falta de optimización o bien con la introducción de errores que darán en algún momento la cara.
En Rust, la memoria se vuelve parte de la arquitectura. Esto me costó mucho comprenderlo (después de estar muy viciado con C# y Javascript con Node).
En realidad, esto es una ventaja… si aceptas algo chocante al comienzo:
- No existe un recolector de basura que amortigüe el coste de decisiones vagas.
- La vida de los datos no es una consecuencia: es un contrato. Puff, otro concepto que digerir.
Esto es, explícitamente hay que indicar para qué vas a usar un variable o recurso, cómo y cuál va a ser su ciclo de vida en la aplicación.
Esta es la primera gran fricción: en Rust, el lenguaje te pregunta “¿quién es el dueño de esto?”, y lo hace porque quiere garantizar algo muy concreto: que su liberación sea segura, determinista y eficiente, y que se haga lo antes posible para liberar recursos. Esto se consigue directamente por la misma naturaleza del lenguaje.
Para ello, se introduce el concepto de propiedad (ownership), mover (move) y préstamo (borrow) de variables y recursos:
Ownership, en realidad, es un concepto muy simple, por el que (simplificando un poco):
- Cada valor tiene un único dueño.
- Cuando el dueño sale de scope (contexto), el valor se libera, y punto; esto es, los recursos que utiliza (memoria) es liberada inmediatamente.
- El valor puede cambiar de dueño (lo que se denomina "mover").
- Al prestar un valor, no se transfiere su propiedad.
Y ya está, ideas simples pero con un impacto poderoso.
En el siguiente snippet se puede ver todo lo anterior (y creéme, al principio es muy frustrante ver cómo para unas simples líneas de código, el compilador no para de protestar hasta que corrijes lo que está mal):
fn main() {
let s = String::from("El Libro Negro del Programador"); // s es el dueño de esa cadena de texto
let t = s; // move: ahora t es el dueño
println!("{}", t);
println!("{}", s); // error: s ya no tiene el valor, el compilador protesta
}
Pero lo importante es que ese error en el código, se detecta en compilación, no en ejecución, y esto tiene un impacto extraordinario en la seguridad y eficiencia de tu aplicación.
Cuando el compilador de Rust indica que está todo bien, te garantiza que formalmente, el código es correcto, al menos desde el punto de vista de data races, asignaciones erróneas de valores, y mil cosas más.
3) Mutabilidad y aliasing: cuando "&mut" es una promesa de exclusividad
Si vienes de lenguajes donde puedes tener múltiples referencias y mutar casi en cualquier sitio, Rust te parecerá extraño y hasta hostil. Pero la regla clave es elegante cuando la comprendes:
- Puedes tener muchas referencias inmutables ("&T") a un mismo valor.
- Solo puede existir una referencia mutable a un valor ("&mut T").
Para los que conocen C o C++, les sonará "&", pero en Rust no existe el concepto de "puntero" (pointer), es tan solo indicar una referencia a un valor y el derecho de leerlo o cambiarlo.
Esto parece un obstáculo hasta que lo miras como lo que realmente es: un principio de diseño para eliminar data races y estados intermedios ilegales que pueden provocar errores catastróficos al ejecutar la aplicación.
Lo improtante es que Rust te obliga a declarar cuándo algo puede cambiar, y la consecuencia natural de esto es que no solo se ayuda al compilador, sino también al lector del código a comprenderlo y, también, al diseño de la aplicación.
Pero todo tiene un coste: hay que pensarlo todo antes. Al principio, como digo, es muy frustrante comprobar cómo para cualquier pequeño código saltan tantos errores en el compilador, hasta que integras las reglas anteriores y comienzar a enfocar el código de esta forma de manera natural.
4) Tipos como contratos: "Option" y "Result" no son “verbosidad”, son dominio
Acostumbrado a manejar excepciones en C#, y casi de forma opcional, llegas a Rust y descubres que en su propia sintaxis está la exigencia de tratar los errores cuando una función puede fallar por alguna razón.
Al principio, esto es muy molesto, pero finalmente te das cuenta de que gracias a esto, tu aplicación, de forma innata, consustancialmente, presenta menos posibilidad de error o de introducir bugs, porque te obliga a gestionar y pensar en todas las situaciones.
En Rust, los errores y la ausencia de valor no son excepciones ni nulos que aparecen en runtime, sino que se gestionan en tiempo de compilación con los traits Result<V,E> y Option<T>. En Rust, por diseño es imposible utilizar un valor que se ha quedado nulo.
Con esto, lo que Rust te está obligando es a expresar la posibilidad de que algo pueda fallar y a manejarlo en el código.
La disciplina aquí no es estética. Es operativa e innerente al lenguaje.
5) Traits y genéricos: no es polimorfismo bonito, es diseño estructural
Si el ownership te hace sudar, los traits suelen venir después con su factura particular, especialmente cuando aparecen bounds, lifetimes, y tipos asociados.
El salto mental importante es el siguiente:
- En muchos lenguajes: “heredo para reutilizar”.
- En Rust: “expreso capacidades para componer”.
Otro punto sutil de comprender, sobre todo su impacto en el diseño de la aplicación.
Traits son capacidades, contratos de comportamiento, y Rust te deja elegir la forma de polimorfismo según lo que necesites:
- Estático (genéricos): más rendimiento, monomorfización.
- Dinámico ("dyn Trait"): flexibilidad.
Hasta aquí es simple. Lo que cuesta es entender que el diseño en Rust se basa menos en jerarquías y más en composición de capacidades.
Cuando lo aceptas, dejas de pelearte con el lenguaje y empiezas a usarlo como herramienta de arquitectura.
6) El compilador no solo compila, sino que es un verificador formal ligero
Rust tiene una relación particular con el compilador. En otros lenguajes, compilar es algo así como pasar un trámite, comprobar que la sintaxis está bien. En Rust, compilar es una conversación, a veces frustrante hasta que comienzas a dominarlo.
Los mensajes del compilador suelen ser largos, específicos y yo diría que hasta pedagógicos. Y eso es totalmente deliberado: Rust intenta ayudarte a mantener invariantes que, de otro modo, se te escaparían al runtime provocando seguramente errores.
Pero aquí hay una verdad que te conviene asumir pronto: si el compilador protesta, normalmente no es porque falte una coma (que también), sino porque no has expresado una idea correctamente.
Cuando el borrow checker no te deja avanzar, no está señalando un error de sintaxis: está señalando una ambigüedad en la vida o en el acceso a los datos, lo cual es susceptible y muy probable de provocar errores en ejecución. Algo que en otros lenguajes funcionaría hasta que dejara de hacerlo.
Rust te obliga a resolver esa ambigüedad lo antes posible.
Como dice un coacher al que sigo: "No te lo creas, compruébalo" :-)
7) Concurrencia sin data races: el “superpoder” que tiene un precio
Rust tiene una afirmación implícita: si compila, ciertas clases de data race no pueden existir, como digo, por diseño del lenguaje. Esto no te vuelve invencible, pero sí te da una base increíble.
Por poner algunos ejemplos, aquí te describo dos conceptos protagonistas:
- "Send" y "Sync" (traits automáticos) para transferir y compartir entre hilos de forma segura.
- Propiedad y préstamos para que la concurrencia no sea un festival de aliasing mutable.
El resultado es extraordinario: Rust no arregla la concurrencia, aunque reduce enormemente el espacio de errores que se pueden producir.
De nuevo: más trabajo al principio, pero menos sorpresas después.
Como conclusión, Rust exige dedicar más esfuerzo en tiempo de compilación y te obliga a responder en el código a estas preguntas:
- ¿Quién posee este dato?
- ¿Cuánto va a vivir?
- ¿Quién puede mutarlo?
- ¿Qué ocurre si falla?
- ¿Qué garantiza este tipo?
- ¿Qué capacidades tiene este tipo y cuáles necesito utilizar?
Al principio esto se siente como una especie de burocracia molesta, pero después descubres que era puro diseño para producir un código más eficiente y seguro. Y, con el tiempo, se convierte en una forma de programar que no depende tanto de la memoria humana, sino de invariantes verificables a los que te obliga el lenguaje: a pensar en un conjunto de reglas, simples, casi axiomáticas, pero con un impacto extraordinario.
Rust no te hace escribir más por capricho. Te hace declarar lo que antes dejabas implícito (y mucho más dado a introducir bugs y utilizar ineficientemente los recursos de la aplicación), y ya sabemos que en lo implícito es donde el software suele fallar en silencio.
Leí la siguiente frase sobre Rust que ahora comprendo perfectamente:
"Rust no te enseña solo un lenguaje, sino una manera de pensar en la corrección mientras programas."
Me gusta Rust, ya no solo por todo lo anterior sino por su portabilidad, eficiencia, ecosistema en crecimiento extraordinario, etc.










Rafael Gómez Blanes/-/logos/img/hdl-plataforma.png)











