Tipos y pattern matching en Gleam: el sistema de tipos que evita errores en tiempo de compilación

El sistema de tipos de Gleam es uno de sus argumentos más sólidos frente a Erlang y Elixir. Ambos son excelentes lenguajes, pero dinámicamente tipados: los errores de tipos aparecen cuando el código ya está corriendo. Gleam toma otro camino: inferencia de tipos completa en tiempo de compilación, sin necesidad de anotar todo manualmente y sin sorpresas en producción.

En este artículo vemos cómo funciona el sistema de tipos de Gleam y cómo el pattern matching encaja con él para escribir código que el compilador puede verificar por completo.

Inferencia de tipos: el compilador trabaja por ti

Gleam infiere los tipos sin que tengas que escribirlos en cada variable o función. Puedes añadir anotaciones explícitas si quieres documentar mejor el código, pero no son obligatorias. El compilador las deduce del contexto:

// Sin anotaciones: el compilador infiere todo
fn double(n) {
  n * 2
}

// Con anotaciones explícitas
fn double_typed(n: Int) -> Int {
  n * 2
}

Si intentas pasar un Float donde se espera un Int, el compilador te lo dice antes de ejecutar nada. Sin pruebas manuales, sin logs de producción para cazar el error.

Sin null, sin excepciones: Option y Result

Dos de las fuentes más comunes de errores en producción son los valores nulos y las excepciones no controladas. Gleam los elimina del lenguaje a nivel de diseño.

Para valores que pueden o no existir, Gleam usa Option(a), que tiene dos variantes: Some(a) cuando hay valor y None cuando no lo hay. No hay null ni nil:

import gleam/list

pub fn first_element(items: List(Int)) -> Option(Int) {
  list.first(items)
  // Devuelve Some(valor) o None
}

pub fn main() {
  case first_element([1, 2, 3]) {
    Some(n) -> io.println("El primero es " <> int.to_string(n))
    None    -> io.println("La lista está vacía")
  }
}

Para operaciones que pueden fallar, el tipo es Result(ok, err) con variantes Ok(ok) y Error(err). Nada de try/catch:

import gleam/int

pub fn parse_number(s: String) -> Result(Int, String) {
  case int.parse(s) {
    Ok(n)    -> Ok(n)
    Error(_) -> Error("No es un número válido: " <> s)
  }
}

Custom types y sum types

Los custom types de Gleam permiten definir tipos con variantes, lo que en teoría de tipos se llama sum types o tipos algebraicos. Son la herramienta principal para modelar el dominio de un problema:

type Shape {
  Circle(radius: Float)
  Rectangle(width: Float, height: Float)
  Triangle(base: Float, height: Float)
}

fn area(shape: Shape) -> Float {
  case shape {
    Circle(r)          -> 3.14159 *. r *. r
    Rectangle(w, h)    -> w *. h
    Triangle(b, h)     -> b *. h /. 2.0
  }
}

Si añades una variante nueva al tipo (por ejemplo Pentagon) y no la cubres en el case, el compilador falla. Eso garantiza que ningún caso quede sin tratar, algo que en lenguajes dinámicos solo puedes detectar con tests exhaustivos.

Pattern matching en detalle

El case de Gleam va más allá de comparar valores. Puedes hacer matching de estructuras, desestructurar records, añadir guards y anidar patrones:

type Point {
  Point(x: Float, y: Float)
}

fn classify_point(p: Point) -> String {
  case p {
    Point(0.0, 0.0)        -> "origen"
    Point(x, 0.0)          -> "en el eje X"
    Point(0.0, y)          -> "en el eje Y"
    Point(x, y) if x == y  -> "en la diagonal"
    _                       -> "punto general"
  }
}

Los guards (if condición) permiten añadir condiciones adicionales a un patrón. El _ es el caso por defecto, equivalente al else de otros lenguajes.

let assert para pattern matching que puede fallar

Hay situaciones donde sabes que un valor solo puede tener una variante concreta y forzar el pattern matching completo es innecesario. Para eso existe let assert:

pub fn main() {
  let assert Ok(n) = int.parse("42")
  io.println(int.to_string(n))
}

Si el valor no coincide con el patrón, el proceso falla (crash controlado, el supervisor de OTP lo puede reiniciar). Úsalo solo cuando estés seguro del valor; para el resto, usa case y cubre todos los casos.

Tipos genéricos

Gleam soporta tipos genéricos con parámetros de tipo. Option(a) y Result(ok, err) son ejemplos de la librería estándar, pero puedes definir los tuyos:

type Pair(a, b) {
  Pair(first: a, second: b)
}

fn swap(pair: Pair(a, b)) -> Pair(b, a) {
  Pair(pair.second, pair.first)
}

El compilador verifica los tipos genéricos igual que los concretos. No hay casting, no hay tipos dinámicos que se cuelen.

Por qué esto importa en la práctica

Trabajar con un sistema de tipos así cambia la forma en que se diseña el código. En lugar de escribir guards de validación por toda la lógica, modelas el dominio con tipos que hacen imposibles los estados inválidos. Si el tipo no permite null, no hay null. Si el tipo obliga a manejar el error, no hay error ignorado.

Para los que vienen de Elixir o Erlang, la curva de adaptación es real pero manejable. El compilador es estricto, pero los mensajes de error son claros y te dicen exactamente dónde está el problema. Con el LSP de Gleam 1.7 en el editor, los errores aparecen mientras escribes, no al compilar.

En el siguiente artículo de la serie comparamos Gleam con Elixir en detalle para ver cuándo tiene sentido elegir uno u otro.

Imagen: Pexels / Markus Winkler

COMPARTE ESTE ARTÍCULO

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