Natural Language Processing en React con Rust + WebAssembly
Introducción
En el siguiente posts vamos a ver paso a paso cómo construir un componente capaz de detectar el lenguaje en el que está escrito un texto mediante NLP (Natural Language Processing), todo sin tener que recurrir a ninguna API y por tanto, capaz de funcionar de manera offline. El resultado final se puede ver en esta Live Demo.
¿Qué es WASM?
Para entender qué es WebAssembly, primero debemos entender que es Assembly, Assembly es cualquier lenguaje de bajo nivel que tenga una fuerte correspondencia con el código de máquina, y por tanto, depende generalmente de la arquitectura en la cual ejecutemos el código (ver diferencia x86 entre ARM).
Para ser un poco más precisos, cuando compilamos códigos en lenguajes tales como C, lo que realmente estamos haciendo es (a grandes rasgos) crear un binario con el código en Assembly (por esto mismo no podemos compartir binarios entre x86 y ARM). Si estás interesado en ver cómo luce el código en x86 Assembly, te recomiendo el siguiente link.
WebAssembly, es la contraparte Web de Assembly. Es un formato binario diseñado para ejecutarse en navegadores de forma portable, segura, eficiente y rápida. Al ser un formato binario, se debe escribir en otro lenguaje para que el compilado sea WebAssembly, tal como para con Assembly normal. Los lenguajes más comunes a usar son C/C++, Rust o Go. En esta ocasión usaremos Rust, ya que es un lenguaje de bajo nivel moderno.
Si bien los fundamentos de Rust se escapan de los objetivos de este post, para los que quieran aprender más, aparte de la documentación oficial, recomiendo este canal de Youtube.
Requisitos
Para seguir este tutorial debes tener instalado las siguientes herramientas, disponibles para Windows, MacOS y Linux.
Next.js
La elección de Next.js como framework para React se debe a que es uno de los más completos y por ende, si funciona en Next.js, probablemente también funcionará en CRA, Gatsby, entre otros.
Podemos partir un proyecto en Next.js sin necesidad de tener instalado el CLI, gracias a npx.
npx create-next-app --example with-chakra-ui-typescript with-chakra-ui-typescript-app
O usar el template de Rokketlabs, el cual viene con features como auth gates, y otros que son muy útiles a la hora de hacer una app real.
git clone git@github.com:rokket-labs/next-template.git
Usaremos la segunda opción, ya que nos da la facilidad de volver este proyecto de experimentación en un proyecto real de forma muy rápida.
Vamos a la carpeta del proyecto e instalamos las dependencias.
cd nextjs-template
yarn install
Para correr el proyecto en modo development ejecutaremos yarn dev y veremos algo como esto
Si vamos a nuestro IDE preferido podemos cambiar esta página en src/pages/index.tsx.
Rust
Aún dentro de nuestro proyecto de Next.js, vamos a ir a src y crearemos lo que será el código que compilará a WebAssembly (desde ahora me referiré a él simplemente como WASM).
cd src
cargo new detect-language
cargo run
Volveremos a ver el mensaje “Hello, World!”, ahora tendremos que decirle a rust qué tipo de librería será nuestro proyecto y agregar una dependencia. Para esto agregaremos las siguientes líneas a Cargo.toml.
Notaremos que ahora Cargo.toml nos dará un error. Esto se debe a que nuestro proyecto tiene un main.rs pero en Cargo.toml dijimos que es una librería, por tanto está buscando el archivo lib.rs (Para mayor explicación ver este video). Creamos lib.rs dentro de src/, ya sea con el IDE o mediante la consola.
cd src
touch lib.rs
Con el siguiente snippet podremos llamar la función alert de Javascript dentro de Rust, a modo de hello world.
Antes de seguir deberemos instalar wasm-pack para poder compilar a WASM. Una vez instalado corremos:
wasm-pack build
Creará una carpeta llamada pkg donde se encontrarán todos los archivos necesarios para importar nuestro módulo WASM. Podemos ver que incluso crea un archivo de definición de types, el cual nos resultará sumamente útil para usar con Typescript.
Back to Next.js
Una vez creado el módulo de WASM, podemos usar el método dynamic que exporta Next.js, el cual nos ayuda a usar dynamic imports. Si no estás usando Next.js, loadable-components hace la misma función. Editamos src/pages/index.tsx
, agregando las siguientes líneas.
Como podemos notar, tenemos types out-of-the-box 🔥.
Ahora usaremos nuestro RustComponent en alguna parte de la página, en mi caso lo pondré justo abajo del Hero.
Al clickear el botón veremos lo siguiente.
Natural language processing
Para detectar en qué lenguaje está escrito un texto usaremos el package whatlang, el cual está basado en el siguiente paper. Agregamos whatlang a las dependencias, por lo que la lista debería verse así
Reemplazamos el ejemplo que teníamos en lib.rs por lo siguiente
Volvemos a hacer wasm-pack build
y actualizamos el componente en React. Para esto agregaremos un Textarea (chakra-ui) y pasaremos el value a RustComponent.
También actualizamos RustComponent para que acepte text como prop
Nuestra página ya detecta el lenguaje!
Ahora, si borramos el texto, nos da un error que no ayuda mucho.
Por suerte, la gente de rustwasm creó un package para tener errores más descriptivos, llamado console__error_panic_hook. Lo agregamos a Cargo.toml y lo usamos en lib.rs
Ahora sabemos que el error se debe a que unwrap asume que la función no dará error, pero cuando no puede detectar que lenguaje es (porque simplemente no tiene texto), nos da un error del cual no se puede recuperar (panicked).
Existen muchas formas de error handling en Rust, pero la que usaremos en este tutorial es similar a try catch en Javascript. El keyword match detecta si una expresión tuvo error o no, en caso de no tenerlo, extraemos el lenguaje y en caso de tenerlo, retornaremos un texto como fallback. Como ya no tendremos error, podemos eliminar el console error panic hook.
Nuestra app ya no crashea y lo mejor de todo es que el binario de WASM solo pesa 279 KB!
Conclusión
Mediante este PoC se puede ver reflejado el nivel de madurez al que ha llegado WebAssembly y su integración, en este caso, con React.
Ejemplos como lo son Google Earth o Figma nos demuestran que se puede utilizar a nivel producción, dotándonos así de una manera de crear códigos seguros, eficientes y agnósticos a la arquitectura del procesador.
Esta interoperabilidad, ha dado paso a proyectos como Wasmer (Run any code on any client) o Polkadot (A scalable, interoperable & secure network protocol for the next web).
Finalmente, solo queda agregar que WebAssembly nos abre las puertas a una enorme cantidad de tipos de proyectos, que antes eran simplemente impensables.