Algoritmos genéticos: un caso práctico

Existen muchos tipos de algoritmos. Hoy vamos a hablar de un tipo de algoritmo no demasiado conocido, los algoritmos genéticos, que son de la familia de los algoritmos evolutivos. Además veremos su aplicación práctica en el vectorizado de imágenes con un programa que he realizado en Rust.

¿Qué es un algoritmo genético?

Un algoritmo genético es un algoritmo inspirado en el mecanismo de la naturaleza de la evolución biológica, descrita por Darwin allá por 1859 en el libro El Origen de las Especies. La idea es tener un conjunto de individuos (posibles soluciones). Estos individuos son evaluados para ver qué tan buenos son. Quedarnos con los mejores y proceder a la creación de nuevos individuos como resultados de la recombinación genética de dos individuos (como en la reproducción sexual). Posteriormente añadir alguna mutación genética para explorar nuevas posibilidades ligeramente diferentes. Proceder de nuevo a la selección natural hasta que tengamos individuos lo suficientemente buenos para nosotros.

Normalmente no son los algoritmos más eficientes ni tienen por qué dar un resultado óptimo, pero pueden servir para dar aproximaciones bastante buenas al resultado óptimo. Existen estrategias para mejorar los algoritmos genéticos como la gestión de la antigüedad de los individuos, para evitar máximos locales; la conservación de individuos con mal desempeño, para conservar mayor variedad genética; …

Como veremos más adelante, uno de los elementos más importante de estos algoritmos es la función de evaluación, que muchas veces es más complicada de programar de lo que parece.

Un caso práctico: vectorizado de imágenes

Para ver como estos conceptos teóricos funcionan en la práctica, vamos a hacer un programa que vectorice imágenes. ¿Esto qué es? Digamos que hay dos tipos de imágenes en informática. Por un lado tenemos las imágenes que almacenan los colores en píxeles (rasterizadas) y por otro lado aquellas que almacenan la imagen como fórmulas matemáticas, que se aplican cuando se quiere ver la imagen (vectoriales). Los formatos más comunes de imágenes rasterizadas son JPEG, PNG, GIF y WEBP. Los formatos más comunes de imágenes vectoriales son SVG y AI.

A las imágenes rasterizadas no se les puede hacer zoom hasta el infinito, se ven borrosas
A las imágenes vectoriales se les puede hacer zoom infinito, no pierden calidad

Pasar de una imagen vectorial a una rasterizada es trivial, pero el proceso inverso no lo es, y es justo donde vamos a aplicar el algoritmo genético.

En nuestro caso, vamos a tomar siluetas, negras sobre fondo blanco y tratar de vectorizarlas con curvas de Bézier.


Ejemplo de ejecución en la versión final de Mendel Vectorizer. La curva azul es la imagen vectorial generada.

Curvas de Bézier

En los años 60, Pierre Bézier, que trabajaba para Renault, diseñó un tipo de curva para el diseño asistido por ordenador (CAD). Estas son las conocidas como curvas de Bézier. Nuestro algoritmo tratará de encontrar la curva de Bézier más similar a la curva de la imagen rasterizada. Para ello necesitamos un punto de inicio, un punto final y dos puntos de control.

Curva cúbica de Bézier

En nuestro algoritmo, las curvas serán los individuos, y las coordenadas de los puntos de control serán los genes (habrá 4 genes por tanto).

Este es el código que he usado para definir las curvas de Bézier en Rust.

Encontrar puntos iniciales

El primer paso de nuestro algoritmo es buscar los puntos iniciales (y finales) de las curvas. En definitiva esto es un problema de búsqueda de esquinas.

Ejemplo de aplicación de FAST9 a una imagen

Existen varios algoritmos de búsqueda de esquinas: Harris, FAST9, FAST12, … No obstante, no tuve muy buenos resultados en las imágenes con las que trabajaba. Así que esta parte del algoritmo se la dejo al humano. El humano se encargará de poner los puntos, en orden, que tiene que tratar de unir el algoritmo con curvas de Bézier.

Función de evaluación

Muchas veces la función de evaluación es el punto más delicado de estos algoritmos. En este caso la idea fundamental es identificar si la curva de Bézier está encima de la curva original. Para ello tomamos 100 puntos equidistantes de la curva de Bézier (con la ecuación paramétrica de la curva es muy sencillo).

\(\mathbf{B}(t)=\mathbf{P}_0(1-t)^3+3\mathbf{P}_1t(1-t)^2+3\mathbf{P}_2t^2(1-t)+\mathbf{P}_3t^3 \mbox{ , } t \in [0,1].\)

Estos puntos se comparan con la imagen real, si ahí en la imagen original había una línea no pasa nada, si no, se resta 100. De este modo se penaliza gravemente salirse de la curva. Esto se hace así ya que la otra opción evidente (bonificar el estar sobre en la línea) podría dar lugar a resultados inesperados.

Ejemplificación de la función de evaluación. Los puntos amarillos están dentro de la línea. Los puntos verdes están fuera de la línea, penalizando a la curva en su “adaptación al medio”.

Pongamos como ejemplo una función de evaluación que bonifique por estar sobre la línea y no penalice por salirse de esta. Una línea bien adaptada a estas condiciones podría recorrerse toda la imagen, cruzando todas las líneas posibles, generando un garabato totalmente inútil pero muy bueno según esta función. Por ello, nuestra función de evaluación se basa en penalizar las salidas de la línea.

La función de evaluación presentada no es perfecta, ya que puede pasar de largo el punto final y dar la vuelta. Esta curva es más larga que la óptima, pero al estar completamente dentro de la línea negra original, la función de evaluación no la puede clasificar como peor que otras alternativas. Para solventar este problema una idea es que la longitud de la curva tenga una implicación en la función. No obstante, el cálculo de la longitud de una curva de Bezier es demasiado complejo y no lo he codificado. También podría aproximarse a través de segmentos rectos entre los 100 puntos calculados anteriormente.

Ejemplo de curva con puntuación máxima pero no óptima desde el punto de vista humano

Selección natural

La función de selección natural deja únicamente las 500 mejores curvas, de acuerdo a la función de evaluación, es decir, las mejor adaptadas de momento. Para la ordenación, Rust usa un algoritmo denominado Timsort, con coste O(nlogn). Sin embargo, en todo el algoritmo trabajamos con poblciones finitas, por lo que puede asimilarse a una constante, con coste O(1). 

Población inicial

La población inicial se forma con 1000 curvas generadas con parámetros totalmente aleatorios. Los valores de cada coordenada, eso sí, están comprendidos entre -d y d siendo d la distancia en línea recta entre los puntos de inicio y final.

Recombinado

El proceso de recombinación permite mezclar los mejores especímenes tratando de conseguir uno mejor. Este algoritmo genético es de tipo RCGA (Real Coded Genetic Algorithm) ya que los genes son números reales, en contraposición a los típicos genes binarios.
Para estos algoritmos existen distintas variantes, aquí se usa el sistema de blend. El sistema de blend implica que de entre los dos padres se toman los valores mínimos y máximos para cada coordenada. Posteriormente se elige un nuevo valor de forma aleatoria con la condición de que esté dentro del rango de mínimo y máximo definido anteriormente.

Mutación

La fase de mutación permite generar pequeñas variaciones aleatorias respecto a la población. Afecta al 10% de la población aunque solo afecta a una coordenada a la vez.

Al ser un algoritmo RCGA, no vale simplemente con cambiar el valor de un bit. El enfoque utilizado en este algoritmo es el de una distribución normal de cambios de media 0. La distribución tiene la forma N(0,d/2). Esto implica que en la mayoría de las ocasiones la variación (tanto positiva como negativa) en la coordenada será bastante pequeña.

Distribución normal, aunque esta no es de media 0

El programa: Mendel Vectorizer

El programa definitivo, Mendel Vectorizer, disponible en GitHub, tiene más detalles. La interfaz gráfica está hecha usando GTK, la interfaz de línea de comandos usa Clap y el algoritmo se ejecuta en paralelo usando paso de mensajes entre los hilos. El programa genera un fichero SVG resultado que puede ser abierto en programas como Inkscape o Adobe Illustrator.

El fichero generado por Mendel Vectorizer en Inkscape

Para usar Mendel Vectorizer en tu ordenador, sigue los siguientes pasos.

Descárgate una copia del código con Git. Compila con Rust (una edición que soporte la edición 2018, mínimo la 1.31) y ejecútalo.

También podéis descargaros la memoria en PDF del programa.

Bindings entre Rust y C/C++ con bindgen

Rust es un lenguaje con muchas posibilidades pero existe mucho código ya escrito en librerías de C y C++. Código que ha llevado mucho tiempo, que ha sido probado en miles de escenarios, software maduro y que no compensa reescribir. Afortunadamente podemos reutilizar ese código en nuestras aplicaciones Rust a través de bindings. Los bindings no son más que trozos de código que sirven de pegamento entre lenguajes. Esto es algo que se ha podido hacer desde siempre pero dependiendo de la librería podía llegar a ser muy tedioso. Afortunadamente tenemos bindgen, un programa que permite generar estos bindings de forma automática analizando el código de la librería de C o C++.

En este post veremos como usar SQLite desde Rust usando bindgen.

Instalando bindgen

En primer lugar necesitamos tener instalado Clang 3.9 o superior. En Ubuntu o Debian necesitamos estos paquetes:

Para el resto de plataformas puedes descargar el binario desde la página de descargas de LLVM.

Bindgen permite dos modos de uso: línea de comandos o desde el código Rust. El más habitual es desde código Rust pero antes veremos el modo en línea de comandos.

Modo línea de comandos

Para bindings sencillos podemos usar el modo línea de comandos. Instalamos binden con Cargo:

Su uso es muy sencillo:

Simplemente indicamos el fichero de cabecera que queremos traducir y su correspondiente fichero de salida en Rust. Este fichero será el pegamento. Vamos a crear un programa que use este pegamento:

Como se puede apreciar, las llamadas al módulo de pegamento de hacen desde un bloque unsafe ya que se van a usar punteros al estilo C, de forma insegura. Hace tiempo escribí sobre ello así que voy a saltarme esa parte.

Compilamos enlazando de forma manual libsqlite3 de la siguiente forma:

Si todo va bien, compilará aunque con numerosos warnings. En principio no son importantes.

Ahora si ejecutamos el programa resultante debería crear una base de datos nueva con una tabla contacts y los datos insertados.

¡Hemos conseguido llamar a una librería de C desde Rust y no hemos escrito ningún binding!

Build.rs

El sistema anterior funciona, pero no es lo más práctico, además no usa Cargo que es el sistema estándar de construcción de programas y crates un Rust. Lo habitual es dejar este proceso automatizado en el fichero build.rs que se ejecuta con Cargo.

Lo primero es añadir la siguiente línea al fichero Cargo.toml:

El siguiente paso consiste en crear un archivo cabecera de C que a su vez haga referencia a todos los archivos de cabecera que necesitamos. En el caso de SQLite es bastante simple.

Y lo llamamos wrapper.h

Ahora viene lo interesante. Dentro de build.rs creamos un programa que gracias a la API de bindgen haga lo mismo que la línea de comandos.

El archivo build.rs debe estar en la misma carpeta que Cargo.toml para que funcione.

Finalmente para hacer accesible nuestros bindings creamos un módulo llamado sqlite.rs con el siguiente contenido.

Lo único que hace es desactivar unos warnings molestos e incluir el texto de bindings.rs al completo.

Una vez hecho esto podemos usar desde el programa principal la librería de la misma forma que hemos visto antes.

Ahora podríamos usar estos bindings directamente en nuestro programa o rustizarlos (darles una capa segura alrededor e idiomática) y subirlo a Crates.io.

El código del post está en GitHub

Futures y Tokio: programar de forma asíncrona en Rust

Rust es un lenguaje muy potente y uno de sus principales focos de diseño es la concurrencia. Sin embargo, si damos un repaso por la librería estándar veremos un par de cosas nada más, algo que puede resultar decepcionante. Esto se debe a que Rust no impone ningún modelo específico de concurrencia como sí lo hacen Go o Erlang. Rust nos provee de los ladrillos de construcción necesarios.

De forma estándar, Rust nos provee de Mutex, atómicos, threads, variables de condición, RwLock y un modelo algo más avanzado de mensajería mediante canales de múltiples productores y un único consumidor (mpsc). En el exterior tenemos crates como Actix que nos proveen del modelo de actores en Rust (dicen que es similar a Akka, yo lo desconozco), modelos de mensajería por canales más flexibles (mpmc) y una cosa muy interesante llamado Futures. Los futures o futuros no son esos contratos que se realizan en bolsa sobre el valor futuro de una acción, sino que son una manera cómoda de manejar valores que existirán en el futuro. Si has usado JavaScript o C# igual te suenan las Promises o Promesas y los Task respectivamente. Los futuros de Rust son exactamente lo mismo.

¿Qué es un futuro?

Un futuro es una variable que representa un dato que todavía no existe. Tenemos la promesa de que ese valor existirá en el futuro. La ventaja es que podemos usarlo aun cuando todavía no tenga un valor. Esto además permite escribir código asíncrono con facilidad.

Una diferencia con respecto a los Promises de JavaScript es que los futuros se basan en poll en vez de en push. Es decir, los futuros van a ir preguntando si el valor ya está disponible. Ante esta pregunta se puede responder con un error, con todavía no está disponible y con ya está disponible, aquí tienes.

Veamos un ejemplo muy tonto pero que puede servirnos para entender algunas cosas.

Definimos un nuevo futuro, SumFuture, que devuelve el resultado de una suma. El futuro en su función poll duerme el hilo 1 segundo y después devuelve el resultado correcto. En la función main llamamos a suma que devuelve un futuro en vez del resultado. Con el futuro, esperamos con wait a que se resuelva y lo mostramos. Cuando un futuro se ejecuta se convierte en una tarea. Las tareas necesitan ejecutores. Wait ejecuta los futuros en el mismo hilo desde el que se hace la llamada, pero existen otras opciones. El programa, tarda un segundo en imprimir el número.

Pero esto no sirve para nada

Bueno, quizá ahora parezca una tontería, pero en cuanto introduzcamos más elementos, todo tendrá más sentido.

Una característica de los futuros es que se pueden encadenar, tal que así:

Ejecutores

Los futuros nos ayudan con la concurrencia, esto es porque se puede esperar a varios futuros a la vez sin problema. Una manera de hacerlo es con CpuPool, un ejecutor que tiene un pool de hilos ya creados. Su uso es muy sencillo, en este ejemplo vemos como hago dos operaciones en paralelo:

Con spawn añadimos un nuevo futuro a la ejecución y nos devuelve otro futuro. Con join podemos unir dos futuros en uno solo que se resuelva cuando ambos hayan resuelto. Finalmente hacemos wait en el hilo principal para esperar a que las sumas se hagan e imprimir el resultado. Existen otros ejecutores. Con Tokio usaremos otro.

Tokio

Tokio es una librería que nos permite ejecutar código de entrada/salida de forma asíncrona, usando futuros por debajo. Son especialmente útiles, la parte de red de Tokio y sus ejecutores correspondientes.

En este ejemplo tenemos un pequeño servidor TCP que se mantiene a la espera de clientes, imprime un mensaje y cierra la conexión. Este servidor solo utiliza un hilo, no obstante, ya usa futuros. server es un futuro infinito, que nunca acaba, que ejecutamos en el hilo actual con run->spawn. El run solo es necesario para ejecutar el primer futuro, el que mantendrá viva la aplicación. spawn empieza a ejecutar el futuro, que entonces se pasa a denominar tarea. Aquí es Tokio en vez de CpuPool, el planificador de tareas.

Servidor de Echo en Tokio

Ahora vamos a ir con un ejemplo más interesante, ahora el cliente y el servidor estarán conectados durante un tiempo, mandando el cliente un mensaje y el servidor respondiendo. Pero queremos que haya varios clientes simultáneamente. Usando futuros y Tokio podemos hacerlo en un mismo hilo (al estilo Node.js).

¿Qué hacemos ahora? Ahora a cada socket de cliente asignamos una tarea, que se encarga, de forma independiente, de ir leyendo línea a línea (gracias a LineCodec).

A partir de aquí se programa en forma de stream, un patrón muy común en la programación con futuros.

En el primer map imprimimos la línea en el servidor y la seguimos pasando por el stream. El siguiente paso es escribir en writer, con forward imprimimos y mandamos esa línea al cliente. Forward a su vez devuelve una tupla con datos (útil para seguir haciendo cosas). Como no los necesitamos, hacemos un map cuya única finalidad sea descartar los valores y finalmente un map_err para capturar posibles errores. Una vez hecho esto tenemos un futuro listo para ser ejecutado. Iniciamos la tarea con spawn y nos olvidamos, pasando a esperar a por un nuevo cliente.

Ahora, en el servidor podemos manejar varios clientes a la vez, cada uno en una tarea distinta, dentro de un mismo hilo.

Cancelando la tarea

Esta tarea que maneja el cliente es infinita, es decir, no se para. La razón es que reader es un lector que genera un futuro infinito esperando a nuevas líneas. Pero, ¿existe algún método para parar la tarea desde el servidor?

Esto no es algo demasiado trivial, pero se puede hacer de manera sencilla usando canales MPSC de Rust y futuros.

La idea aquí es hacer que de alguna manera, el futuro pueda resolverse y así finalizar la tarea. Una función interesante es select2 que devuelve un futuro fusión de dos futuros. Este futuro se resuelve (devuelve valor y acaba la tarea) cuando alguno de ellos lo haga. Como el futuro de reader nunca acabará, entonces basta con poner un futuro que cuando queramos cerrar la conexión resolveremos.

Este futuro es cancel de tipo Cancellable. No viene en la librería, lo he creado arriba y lo que hace es esperar a que el extremo del canal reciba una comunicación. El valor nos da igual, simplemente que se haya producido un ping. Una vez hecho eso, el futuro resuelve y la conexión se cierra.

En el ejemplo, cuando el cliente manda bye la conexión se cierra.

Y esto es una breve introducción a los futuros y a Tokio en Rust. Todavía queda ver como podemos usar async/await para no tener que escribir todo esto en forma de stream.

Lenguajes de programación que todo buen programador debe conocer

Dice Bjarne Stroustrup (creador de C++) que nadie debería llamarse un profesional si no conoce al menos 5 lenguajes suficientemente diferentes entre sí. Comparto con él esa afirmación, así que he decidido hacer una lista con esos 5 lenguajes suficientemente diferentes entre sí. La razón de que sean diferentes entre sí es que implementan paradigmas distintos.

Paradigmas de programación

Cada lenguaje está moldeado en base a uno o varios paradigmas de programación. Aunque no hay una teoría con la que todos los autores esten de acuerdo, bajo mi punto de vista existen dos grandes grupos de paradigmas de programación. Imperativos y Declarativos. Los imperativos responden a la pregunta de ¿Cómo se calcula esto? y los declarativos ¿Cuál es el resultado de esto?. Otra manera de verlo es ver al paradigma imperativo como un intento de simplificar la electrónica subyacente. El paradigma declarativo por contra muchas veces se origina de la teoría matemática y posteriormente se aplica al ordenador.

Cada uno de estos paradigmas a su vez tienen más sub-paradigmas y luego existen paradigmas transversales, es decir, que se pueden aplicar (o no) tanto en lenguajes imperativos como en lenguajes declarativos.

Un buen programador necesita conocer estos paradigmas.

Prolog

Prolog es un claro ejemplo de programación lógica. Se trata de un lenguaje declarativo. Diseñado en los años 70 en Francia, Prolog tuvo mucha popularidad en el desarrollo de Inteligencia Artificial debido a sus características lógicas. En esencia, Prolog se basa en la demostración de predicados, similares a los del álgebra de predicados.

Ejemplos de lógica de predicados

Un programa Prolog es en realidad un conjunto de afirmaciones o predicados. En tiempo de ejecución se realizan preguntas sobre predicados. Prolog intenta entonces demostrar la veracidad del predicado, para ello usa el mecanismo de backtracing. Una característica muy interesante de Prolog es el pattern matching, que básicamente permite preguntar para qué valor de una variable se cumple un predicado. Esto permite realizar cosas muy interesantes:

Ahora, para saber quién es la madre de Sonia intentamos demostrar:

Y responderá X = veronica.

En predicados sin variables, Prolog solo devuelve true o false.

Existen varios compiladores/intérpretes de Prolog, siendo el más conocido SWI-Prolog, multiplataforma y con una extensa librería que incluye GUI multiplataforma y framework web. También existe GNU Prolog y Visual Prolog (antiguamente conocido como Turbo Prolog), aunque este último no se le considera Prolog auténtico por ser demasiado diferente al resto.

Haskell

Haskell es un lenguaje declarativo que implementa el paradigma funcional. Es uno de los pocos lenguajes funcionales que son 100% puros. Se entiende por puros como la capacidad de no generar efectos colaterales. Haskell es un lenguaje fuertemente tipado y deriva de la teoría de categorías. Haskell ha sido objeto (hasta cierto punto merecido) de muchas bromas sobre este asunto, ya que para la mayoría de programadores, conocer teoría de categorías no es demasiado práctico.

Otra característica de Haskell es que es perezoso, lo que significa que no calculará nada que no sea estrictamente necesario (esto puede parecer muy raro hasta que lo entiendes en la práctica).

Este código pertenece al juego de la vida de Conway que (también) implementé en Haskell, simplemente por curiosidad, ya que no está optimizado. Faltaría el módulo Reader, así que no intentéis compilarlo directamente.

Haskell como tal no tiene variables ni bucles y sus condicionales no son exactamente iguales a los de los lenguajes imperativos (aunque se use if, en el caso de Haskell son bloques que siempre deben devolver un valor).

Aunque siempre ha sido un lenguaje académico (nació en 1994, un año antes que Java), ahora ha alcanzado bastante popularidad y algunas empresas como Facebook lo usan en producción.

El compilador más conocido, capaz de generar código nativo, es GHC. Este dispone de un REPL llamado GHCi y también compila a JavaScript y se está trabajando en WebAssembly. Otro intérprete es Hugs, pero solo es compatible con Haskell98.

La forma recomendada de instalar GHC es con Stack. Usa Stack y ahórrate quebraderos de cabeza.

Otros lenguajes similares a Haskell son ElmPureScriptEta e Idris. Este último compila a JavaScript y pone énfasis en su librería, que es capaz de competir con Angular y React.

Racket (Lisp)

Lisp no puede faltar nunca. Se trata de uno de los primeros lenguajes de programación y sigue siendo tan actual como el primer día, gracias a su simple pero efectivo diseño, inspirado en el cálculo lambda de Alonzo Church. No obstante, Lisp ha evolucionado mucho, si me preguntan que dialecto/lenguaje de Lisp merece la pena aprender ahora diría Racket. Racket es un lenguaje de programación funcional y programación orientada a objetos. Racket no es 100% puro como Haskell (la mayoría de dialectos de Lisp no lo son) pero sigue siendo muy interesante. También, tiene tipado débil en contraposición al tipado fuerte de Haskell.

Racket desciende a su vez de Scheme, que es una de los ramas principales de Lisp, siendo la otra Common Lisp. ¿Por qué estas diferencias? La gente de Scheme prefiere un lenguaje elegante, lo más funcional posible mientras que la gente de Common Lisp prefirieron sacrificar eso a cambio de un lenguaje más práctico en el mundo real.

Racket cuenta con una extensísima librería estándar capaz de realizar todo lo que te imagines sin gran problema. Racket también soporta las famosas y potentes macros.

Triángulo de Sierpinski en el IDE DrRacket.

JavaScript

JavaScript está en todas partes. Es uno de los lenguajes con mayor aplicación práctica. Web, servidor, bases de datos, scripting, plugins e incluso IoT. Por eso JavaScript me parece un lenguaje que deba estar en esta lista. Y sin embargo no es fácil categorizarlo correctamente. Ante todo, estamos ante un lenguaje de programación imperativo, con orientación a objetos y buen soporte a la orientación a eventos.

Y antes de que se me venga alguien a comerme, sí, JavaScript está orientado a objetos, aunque no siguen el patrón de clase/herencia que C++ y Java tienen tan acostumbrados a la gente. La orientación a objetos por prototipos no la inventó JavaScript, sino que ya estaba en otros lenguajes como Self y más actualmente Io. Y realmente lenguajes como Python o Ruby no se alejan mucho de esto internamente.

Actualmente, con la versión ES7, tenemos muchas cosas interesantes en programación asíncrona y clases al estilo Java que no son más que azúcar sintáctico sobre el verdadero modelo de JS.

Definitivamente, JavaScript es un lenguaje muy interesante y aunque a algunas personas les pueda parecer un caos, ciertamente es muy productivo y útil. Aprovechar al máximo JavaScript requiere pensar de forma distinta a como lo harías en otros lenguajes imperativos.

C# (o Java)

C# es otro lenguaje bastante complejo, imperativo, orientado a objetos por clases, con partes de programación funcional y programación asíncrona. Pero el sistema de la orientación a objetos aquí es distinto. Aunque Alan Kay, creador de Smalltalk y casi casi de la orientación a objetos, opine que el estilo de C++ y de Java son putas mierdas, lo cierto es que es lo más usado actualmente. Herencia, clases, interfaces, etc

Personalmente prefiero C#, ya que como lenguaje es más potente que Java, pero ambos al final tienen bastantes características comunes entre sí.

Para C# el compilador más usado es Roslyn, actualmente disponible en .NET Framework, .NET Core y Mono.

Otros lenguajes parecidos son Java o si preferimos una sintaxis tipo Pascal: Delphi/Object-Pascal tiene conceptos muy similares.

Conclusión

Con esto ya tendríamos 5 lenguajes suficientemente diferentes. Ahora bien, nos hemos dejado lenguajes muy interesantes, tanto desde el punto de vista práctico como teórico. No puedo dejar de mencionar a Smalltalk por su implementación de los objetos, a C por su ligera capa de abstracción sobre ensamblador, al propio Ensamblador porque es lo que ejecuta la CPU realmente, a Python por su diseño así como su gran uso en ciencia de datos y scripts o a Rust, un lenguaje imperativo con sistema de traits, semántica de movimiento, pattern matching.

Merece también la pena mirar FORTH, SQL (quizá el lenguaje declarativo más usado del mundo) y lenguajes que se adapten bien a la programación reactiva (hay sobre todo librerías, aunque algún lenguaje como Simulink lo implementa). Mathemathica implementa también el paradigma de programación simbólica, muy poco explotado en otros lenguajes.

Estos han sido mis 5 lenguajes, ¿cuáles serían los tuyos?

Yew, crea webapps al estilo Angular/React en Rust

Hoy os traigo una librería muy potente y muy útil, ahora que Rust compila a WebAssembly de forma nativa. Se trata de Yew, una librería para diseñar frontends, single-page-applications y en general, una librería que es una alternativa a Angular, React, Vue y Elm.

En particular, Yew se basa en The Elm Architecture, por lo que los usuarios de Elm serán los que encuentren más familiar a Yew.

Yew significa tejo. He aquí uno de los tejos más famosos de Valladolid, en la plaza del Viejo Coso

Instalar cargo-web y el soporte a WebAssembly

Antes de empezar a programar necesitaremos instalar un plugin para Cargo, llamado cargo-web, que nos ayudará en el desarrollo web con Rust. Por otro lado, hace falta instalar el soporte de Rust a WebAssembly. Existen tres opciones actualmente: asmjs-unknown-emscripten, wasm32-unknown-emscripten y wasm32-unknown-unknown. Para los primeras opciones hace falta tener instalado Emscripten. Para la última, no hace falta nada, por lo que es mi opción recomendada. Por otro lado, wasm32 significa WebAssembly y asmjs es asm.js, que es simplemente JavaScript y será compatible con más navegadores.

The Elm Architecture

Los principios de The Elm Architecture se basan en: modelo, mensajes, actualización y vista.

Para este tutorial vamos a hacer la típica aplicación de una lista donde guardamos notas. Nuestro modelo se va a componer de una lista de tareas, para simplificar, pongamos que una tarea es simplemente un String y un ID. Entonces también nos hará falta almacenar un contador para ir poniendo IDs. También, al tener un campo de texto, nos hará falta una variable para ir almacenando temporalmente lo que escribe el usuario.

Lo siguiente es diseñar los mensajes. Los mensajes interactúan con el modelo y desencadenan una actualización de la vista. En esta aplicación solo nos hacen falta dos mensajes: añadir mensaje y borrar mensaje. Pero como tenemos un campo de texto, tenemos que introducir un mensaje Change y siempre viene bien un mensaje que no haga nada.

Una vez hecho esto pasamos a crear la función update, en la que hacemos distintas cosas según el mensaje que recibamos.

Así pues, si se lanza un mensaje Msg::Add lo que hacemos es copiar el valor de la variable temporal input, crear una nueva tarea con su ID y añadirla a la lista de tareas. Ya está. Yew mágicamente actualizará la página para reflejar que la lista de tareas ha sido modificada. Lo mismo pasa con Remove.

Ahora vamos a las vistas. Una vista es una función que devuelve Html<Msg> y se pueden componer varias funciones así. En nuestro caso, tenemos una vista principal donde se ve un campo de texto y un sitio donde se ejecuta un bucle for con las tareas del modelo. Y a cada tarea se le aplica la vista view_task.

La macro html! nos permite escribir HTML directamente en Rust, con algunas diferencias (¡prestad atención a las comas!). También nos permite introducir código Rust (entre llaves) y closures (observad onclick, oninput y onkeypress).

Finalmente en el método main, inicializamos el modelo y llamamos a program, que empieza a ejecutar Yew.

El código final queda así.

Ejecutando la aplicación web

Usando cargo web, es muy sencillo generar la aplicación web. Simplemente ejecuta:

El resultado, se montará en un mini servidor web. Si accedes a la URL que indica Cargo con tu navegador web, verás algo similar a esto:

Añade items y bórralos. Observa como la aplicación funciona perfectamente.

Distribuyendo la aplicación

Realmente cargo web ha hecho muchas cosas por nosotros. Si nosotros queremos usar Yew en la vida real, no usaremos cargo web. Para ello, compilamos la aplicación web:

Y accedemos a la carpeta target/wasm32-unknown-unknown/release. Allí encontraremos dos archivos que vamos a necesitar. Uno acabado en .js y otro acabado en .wasm. Ambos ficheros deberemos copiarlos donde queramos usarlos. Por último, será necesario un fichero HTML. En el fichero HTML solo hace falta cargar el fichero JS. Yew hará el resto.

Si quieres saber más, puedes descargarte el código de ejemplo que se encuentra en GitHub.