Concurrencia en Gleam: procesos, actores y OTP desde un lenguaje tipado

La concurrencia es una de las razones principales para usar cualquier lenguaje que corra sobre la BEAM. La máquina virtual de Erlang lleva décadas manejando millones de procesos ligeros concurrentes con tolerancia a fallos incorporada. Gleam hereda todo eso y lo expone con tipos estáticos, lo que añade una capa extra de seguridad a un modelo ya de por sí robusto.

Procesos en la BEAM: lo que Gleam hereda

En la BEAM, un proceso no es un proceso del sistema operativo ni un hilo del SO. Es una unidad de ejecución ligera gestionada por la máquina virtual, con su propio heap de memoria aislado. Puedes tener miles o millones de ellos corriendo simultáneamente sin que el consumo de memoria se dispare.

La comunicación entre procesos es por paso de mensajes. Cada proceso tiene un buzón (mailbox) donde recibe mensajes. No hay memoria compartida, así que no hay condiciones de carrera clásicas ni necesidad de locks.

import gleam/erlang/process

pub fn main() {
  let pid = process.start(fn() {
    // Este código corre en un proceso separado
    io.println("Hola desde otro proceso")
  }, True)
}

El paquete gleam_otp: actores tipados

La librería gleam_otp ofrece una interfaz tipada sobre OTP (Open Telecom Platform), el conjunto de librerías de Erlang para construir sistemas concurrentes y tolerantes a fallos. El componente más usado es el actor.

Un actor en Gleam es básicamente un proceso que mantiene un estado y recibe mensajes de un tipo concreto. El tipo del mensaje está definido en tiempo de compilación, así que no puedes enviar un mensaje del tipo incorrecto sin que el compilador te avise:

import gleam/otp/actor
import gleam/erlang/process

// Tipo de los mensajes que acepta el actor
type Message {
  Increment
  Decrement
  GetCount(reply_with: process.Subject(Int))
}

// Handler: recibe mensaje y estado, devuelve nuevo estado
fn handle_message(message: Message, count: Int) -> actor.Next(Message, Int) {
  case message {
    Increment ->
      actor.continue(count + 1)
    Decrement ->
      actor.continue(count - 1)
    GetCount(client) -> {
      process.send(client, count)
      actor.continue(count)
    }
  }
}

pub fn main() {
  let assert Ok(actor) = actor.start(0, handle_message)

  process.send(actor, Increment)
  process.send(actor, Increment)
  process.send(actor, Increment)

  let subject = process.new_subject()
  process.send(actor, GetCount(subject))
  let count = process.receive(subject, 1000)
  // count = Ok(3)
}

Fíjate en el tipo GetCount(reply_with: process.Subject(Int)): el cliente le pasa al actor un «subject» (un canal con tipo) al que el actor responde. El tipo Int garantiza que la respuesta será un entero, no un átomo ni cualquier otra cosa.

Supervisores: tolerancia a fallos tipada

Un supervisor es un proceso cuya función es vigilar otros procesos y reiniciarlos si fallan. Es el mecanismo central de la filosofía «let it crash» del ecosistema Erlang: en lugar de intentar recuperarse de todos los errores posibles, dejas que el proceso falle y que el supervisor lo reinicie en un estado limpio.

import gleam/otp/supervisor

pub fn start_supervisor() {
  supervisor.start(fn(children) {
    children
    |> supervisor.add(worker_spec())
    |> supervisor.add(another_worker_spec())
  })
}

Gleam wrapalea los supervisores de OTP con tipos, así que la definición de qué procesos supervisa y con qué estrategia también se verifica en compilación.

Paso de mensajes entre procesos

El módulo gleam/erlang/process da acceso al modelo de mensajes de la BEAM. Los subjects son los canales tipados de Gleam: un process.Subject(T) solo puede recibir mensajes de tipo T. Esto elimina toda la ambigüedad de saber qué tipo de mensaje puede llegar en un buzón:

import gleam/erlang/process

pub fn ping_pong() {
  let subject = process.new_subject()

  // Spawn un proceso que envía de vuelta
  let _ = process.start(fn() {
    let assert Ok(msg) = process.receive(subject, 5000)
    io.println("Recibido: " <> msg)
  }, True)

  // Enviar un mensaje al proceso
  process.send(subject, "ping")
}

Por qué los tipos importan aquí

En Erlang y Elixir, el buzón de un proceso puede recibir cualquier mensaje de cualquier tipo. Si envías un mensaje con la forma incorrecta, el proceso puede quedarse procesando mensajes que no entiende o fallar con un error críptico. Con Gleam, el compilador previene eso: si el tipo del mensaje no coincide con lo que el actor espera, el código no compila.

Eso es una diferencia práctica importante cuando tienes sistemas con muchos actores comunicándose entre sí. El refactor de un tipo de mensaje en Gleam te da una lista exacta de todos los lugares donde hay que actualizar el código.

Gleam en sistemas concurrentes reales

Gleam no reinventa la concurrencia: usa exactamente el mismo modelo que Erlang lleva usando desde los años 80. Lo que añade es la verificación de tipos sobre ese modelo. Para sistemas donde la corrección importa, esa combinación tiene mucho sentido: la robustez probada de la BEAM con la seguridad de un compilador que no deja pasar errores de tipos.

Si quieres ver cómo Gleam interacciona con código Erlang y Elixir existente para aprovechar librerías ya hechas, el siguiente artículo de la serie cubre la interoperabilidad con el ecosistema BEAM.

Imagen: Pexels / K

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP