Gleam – Nuevo lenguaje de programación funcional estáticamente tipado en BEAM

El pasado lunes 15 de abril se publicó la primera versión de Gleam (0.1): Gleam es un lenguaje de programación funcional estáticamente tipado diseñado para escribir sistemas concurrentes mantenibles y escalables. Compila a Erlangy tiene interoperabilidad directa con otros lenguajes de BEAM (la máquina virtual de Erlang) como Erlang, Elixir y LFE.

Obviamente es un lenguaje bastante nuevo, así que no está listo para usar en producción. Está interesante y ya se puede probar, programar alguna cosa divertida y aprender a usarlo. El código fuente está disponible en GitHub bajo licencia Apache 2.0, y chat del proyecto se encuentra en IRC en #gleam-lang de Freenode. Aplaudo el uso de IRC para proyectos de Software Libre en vez de Slack 👏
¡El compilador está escrito en Rust! Y si bien no conozco Rust, parece que saca algo de inspiración de ese lenguaje en la sintaxis y le veo también algunas cosas de Elixir.

Principios de Gleam

Ser seguro

Un sistema de tipados expresivo inspirado por la familia de lenguajes ML nos ayuda a encontrar y prevenir bugs en tiempo de compilación, mucho antes que el código llegue a los usuarios.

Para los problemas que no pueden ser resueltos con el sistema de tipos (como que le caiga un rayo al servidor) el runtime Erlang/OTP provee mecanismos bien testeados para manejar fallas.

Ser amigable

Perseguir bugs puede ser estresante así que la realimentación del compilador debe ayudar y ser lo más clara posible. Queremos pasar más tiempo trabajando en nuestra aplicación y menos tiempo buscando errores tipográficos o descifrando mensajes de error crípticos.

Como comunidad queremos ser amigables también. Personas de todos los orígenes, géneros y niveles de experiencia son bienvenidas y deben recibir respeto por igual.

Ser performante

El runtime Erlang/OTP es conocido por su velocidad y habilidad para escalar, permitiendo que organizaciones como WhatsApp y Ericsson manejen cantidades masivas de tráfico de manera segura con baja latencia. Gleam debería aprovechar por completo el runtime y ser tan rápido como otros lenguajes de la BEAM como Erlang y Elixir.

Ser un buen ciudadano

Gleam facilita el uso de código escrito en otros lenguajes BEAM como Erlang, Elixir y LFE, así que hay un ecosistema rico de herramientas y librerías para que los usuarios de Gleam aprovechen.

A cambio los usuarios de lenguajes BEAM deberían poder aprovechar Gleam, ya sea usando librerías escritas en Gleam de manera transparente, o agregando módulos Gleam a sus proyectos existentes con mínimo trabajo.

Instalación

Para instalarlo, debemos seguir esta guía, Necesitamos tener Rust, Erlang y rebar3 instalados de antemano, clonar el código con git, y compilar el compilador. El soporte para editores de texto todavía está un poco en pañales, pero ya hay modos para EmacsVim y Visual Studio Code.

Empezando a programar

Al ser un lenguaje tan nuevo, todavía no es un proceso tan directo crear un proyecto. De todas formas sólo hay que seguir estos pasos. El compilador puede trabajar con proyectos generados con rebar3, una herramienta de builds de Erlang.

Los tipos básicos de Gleam son StringBool (True || && False), Int (enteros) y Float (decimales). Los valores pueden nombrarse con let para ser reusados después, pero los valores contenidos son inmutables:

let x = 1
let y = x
let x = 2
 
x // => 2
y // => 1

Tiene Tuplas, colecciones ordenadas de tamaño fijo con elementos que pueden ser de distintos tipos:

{"Texto", 2, True}

Y acá me empiezo a acordar de Elixir, podemos extraer los valores de la tupla así:

> valores = {"Texto", 2, True}
{"Texto", 2, True}
> {a, b, c} = valores
{"Texto", 2, True}
> a
"Texto"
> b
2
)> c
True

Otra estructura de datos son las Listas: Colecciones ordenadas de elementos del mismo tipo:

[7, 8, 9]
// Y para agregar nuevos valores, como en Elixir, se usa:
> ["N" | ["a", "n", "d", "o"]]
["N", "a", "n", "d", "o"]

Por último están los Mapas, colecciones de nombre y valores que pueden ser de cualquier tipo:

{
  nombre = "Nicanor",
  edad = 20,
}

Y se puede acceder al valor con la sintaxis `mapa.nombre_del_campo`:

let persona = { nombre = "Nicanor", edad = 20 }
let nombre = persona.nombre
 
nombre // => "Nicanor"

Para actualizar o agregar valores a un mapa se usa la sintaxis: { mapa | nombre_del_campo = valor }:

let persona1 = { nombre = "Marcela", edad = 22 }
let persona2 = { persona1 | edad = 23, idioma: "español" }
 
persona1 // { nombre = "Marcela", edad = 22 }
persona2 // { nombre = "Marcela", edad = 23, idioma: "español" }

Los tipos del mapa dependen de los nombres y tipos de los campos. El compilador lleva un registro de los campos y valores de cada mapa y presenta un error en tiempo de compilación en caso de querer usar un campo que no exista o tenga el tipo incorrecto.

Las funciones con nombre se declaran con pub fn:

pub fn sumar(x, y) {
  x + y
}
 
pub fn multiplicar(x, y) {
  x * y
}

Y como son valores de primera clase se pueden asignar a variables, pasar a funciones y todo lo demás que se puede hacer con cualquier otro tipo de datos:

pub fn dos_veces(f, x) {
  f(f(x))
} // Wooooo!

También tiene funciones anónimas con una sintaxis similar:

pub fn ejecutar() {
  let sumar = fn(x, y) { x + y }
  sumar(1, 2)
}

No puedo evitar las comparaciones con Elixir, pero es a lo que me hace acuerdo… Hay una sintaxis corta para crear funciones anónimas que toman un argumento y lo pasan a otra función. Y esto se usa generalmente con pipes para crear una serie de transformaciones de datos:

pub fn sumar(x, y) {
  x + y
}
 
pub fn ejecutar() {
  // esto equivale a sumar(sumar(sumar(1,2), 4), 6)
  1
  |> sumar(_, 2)
  |> sumar(_, 4)
  |> sumar(_, 6)
}

Vistas las funciones otra cosa interesante es pasarles mapas como parámetros. El lenguaje apunta a ser muy permisivo con esto, y muestra un ejemplo de cómo funciona:

fn numero_siguiente(mapa) {
   mapa.numero + 1
}

El tipo de la función es fn({ a | numero = Int}) -> Int. La a en este caso puede ser «cualquier otro campo», así que la función se puede llamar con cualquier mapa siempre y cuando tenga el campo numero con un valor del tipo Int.

let articulo = { nombre: "tenedor", numero: 17 }
let nintendo = { nombre: "gameboy", numero: 4 }
let fernando = { nombre: "Fernando", edad: 33 }
 
numero_siguiente(articulo) // => 18
numero_siguiente(nintendo) // => 5
numero_siguiente(fernando) // => Compile time error! No numero field

La expresión case se usa como estructura de control por medio de la técnica «pattern matching» (ya escribí algo sobre Pattern Matching en el blog antes). Cómo se usa:

case numero {
| 0 -> "Cero"
| 1 -> "Uno"
| 2 -> "Dos"
| n -> "Otro número" // machea todo lo que no sea lo anterior
}

Como alternativa al if else de otros lenguajes, hace pattern matching con los valores Bool:

case alguna_condicion {
| True -> "Es verdadera"
| False -> "Es falsa"
}

La expresión case retorna un valor por lo que podemos asignarle el resultado a una variable. El pattern matching me resulta una cosa mágica y feliz, pero debe ser porque no estoy acostumbrado a usarlo.

El lenguaje también cuenta con Enums, y el tipo Bool está definido como uno:

enum Bool =
  | True
  | False

Una variante que muestra como ejemplo para extraer valores es:

enum User =
  | LoggedIn(String)
  | Guest
 
let diego = LoggedIn("Diego")
let leticia = LoggedIn("Leticia")
let visitor = Guest

Los enums también pueden ser «patternmacheados» para asignarle nombres a distintas variantes y podemos usarlos en un let.
🤯

Por último, al ser un lenguaje de la BEAM, podemos usar funciones de otros lenguajes directamente ¡Santa interoperabilidad Batman! Al ser lenguajes distintos, el compilador no puede determinar el tipo de las funciones, así que es la responsabilidad del programador en esos casos de hacer las cosas bien o veremos hermosas explosiones en nuestra aplicación. Pero llamar funciones de otros lenguajes es bastante sencillo:

// Llamando a la función uniform del módulo rand de Erlang:
pub external fn random_float() -> Float = "rand" "uniform"
 
// Llamando a IO.inspect de Elixir:
pub external fn inspect(a) -> a = "Elixir.IO" "inspect"

Como si fuera poco, también podemos importar tipos externos.

Fuente: https://picandocodigo.net