pensar en rust

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.

Estos últimos meses he estado introduciendo muchas mejoras en el core de Mantra, plantenado el camino para que en los componentes de un proyecto se puedan introducir módulos en formato WebAssembly y programados en Rust, sin que el ciclo de vida del componente se haga más complejo; trabajo en este camino porque precisamente Mantra está planteado para implementar aplicaciones de alto rendimiento y escalables, por la propia naturaleza del framework y el paradigma de desarrollo que sigue.

Llevo profundizando en estas tecnologías desde que comenzó el año, y aunque a lo largo de mi carrera profesional he visto ya varias promesas que afirmaban que "lo cambiarían todo" (escondiendo, seguramente, algún propósito comercial, claro está), para desaparecer en el olvido años más tarde, no puedo decir lo mismo de WebAssembly en general y Rust como lenguaje en particular.

WebAssemblyWebAssembly

¿Por qué? Te lo explico a continuación.

En software ocurre exactamente lo mismo que en otras actividades: es cierto que en ocasiones, una moda pasajera impone que se usen tecnologías (o ciertas librerías o frameworks, que no siempre se distingue bien entre una cosa y la otra) que, en mi opinión, no deberían tener el puesto destacado que tienen, pero en otras, la enorme utilidad de algo hace que explote su uso, porque precisamente viene a llenar un vacío en la industria o a resolver un problema importante.

Eso es precisamente lo que pienso de WebAssembly y Rust que, al igual que el resto de tecnologías, ni resuelven todos los problemas ni son un cajón de sastre para hacer de todo, tienen su espacio y sus casos de uso.

Éstos se basan principalmente en aplicaciones (cada vez con mayor demanda y auge) que necesitan de un rendimiento mucho mayor (cercano al de las máquinas nativas donde se ejecutan) y que no se puede obtener por diversos motivos con otros entornos de programación, pero añadiendo también otras características que, sinceramente, cuando he ido profundizando en ellas me he quedado sorprendido: mayor seguridad por la propia naturaleza de Rust, no hay necesidad de que exista un recolector de memoria (sí, lee esto de nuevo) y, lo más importante, portabilidad a diferentes arquitecturas hardware y sistemas operativos, nada más y nada menos.

Mientras que WebAssembly es un lenguaje de bajo nivel similar al ensamblador (tranquilo, que no hace falta volver a programar en ensamblador), y que genera un formato binario portable y extraordinariamente compacto cuando se compila (generando archivos wasm), que puede ser ejecutado los navegadores de mayor use pero también en otros entornos, Rust es un lenguaje diseñado desde su base para generar código eficiente, con un uso de la memoria seguro y en contextos de threads también seguros, y que recuerda mucho a C  y C++ aunque no es de tan bajo nivel como éstos.

Rust no es un lenguaje escribir para cualquier tipo de aplicación, sino más bien y dada su naturaleza, para programar sistemas: drivers para dispositivos embebidos, sistemas operativos, procesamiento de imágenes y de video, internetworking, virtualización e incluso para programar otros lenguajes de mayor nivel. Otra definición para Rust: es un lenguaje para la programación concurrente sin la dificultad que implica esto en lenguajes como C o C++ y con el rendimiento de éstos.

Por decirlo de alguna manera, WebAssembly empaqueta en ese código binario portable y eficiente el resultado de aplicaciones escritas en Rust (pero también en C y C++).

WebAssembly está fuertemente apoyado por grandes de la industria, como Mozilla, y en mi opinión, aunque cuenta ya con unos años de desarrollo, su uso y aplicaciones se van a extender extraordinariamente en el futuro: tratamiento de imágenes y de video, aplicaciones en tiempo real que manejan grandes volúmenes de datos, aplicaciones embebidas para dispositivos IoT, FaaS (functions as a service), gaming (puesto que éstos son aplicaciones que también requieren de un mayor rendimiento), aplicaciones multiplataforma, etc.

Por otra parte, la curva de aprendizaje del tooling para WebAssembly así como de Rust, en mi opinión es baja, sobre todo si ya has tenido una experiencia previa con entornos basados en C o C++ y has programado mucho a bajo nivel. En una etapa laboral anterior, estuve 12 años programando en C++, de modo que veo en Rust muchas características similares que me llenan de cierta extraña nostalgia :-)

En software frecuentemente se lanzan tecnologías que terminan resultando efímeras (y que seguramente nos ha costado muchas horas aprender), pero WebAssembly y Rust han llegado para quedarse, tan solo hay que ver qué empresas lo usan y lo apoyan y están involucradas en su desarrollo.

Mis libros en todas las tiendas:

Amazon
Google Play
Apple
Kobo
Barnes and Noble
Scribd
Smashwords
Payhip
Gumroad

Rafael Gómez Blanes Rafael Gómez Blanes
¿Hablamos?

 

Archivo

Mis novelas...