Haskell, determinismo y el choque con el mundo real

Elliot Luque
4 min

Hay un momento muy concreto en la vida de casi cualquier persona que estudia informática en el que algo hace click.
No suele ser cuando aprendes un nuevo framework, ni cuando haces tu primera API REST.

En mi caso, ese momento llegó estudiando la asignatura Lenguajes, Tecnologías y Paradigmas de la Programación, concretamente al empezar a trabajar con Haskell.

Hasta entonces, programar era algo bastante intuitivo:

  • escribes código
  • lo ejecutas
  • hace lo que esperas

Pero Haskell empieza a hacerte preguntas incómodas.


El choque inicial: funciones puras y determinismo

En Haskell aparece una idea muy fuerte desde el primer día:

Una función, si es pura, siempre devuelve el mismo resultado para los mismos argumentos.

No “normalmente”.
No “en esta máquina”.
Siempre.

Eso te obliga a pensar el programa como una función matemática, no como una secuencia de pasos que “van pasando cosas”.

Y entonces surge la pregunta inevitable:

Si esto es tan limpio y tan razonable…¿por qué los programas reales fallan tanto?


De Haskell al mundo real

Cuando sales del entorno académico y vuelves a los sistemas reales, te encuentras con todo lo contrario:

  • timeouts que saltan “a veces”
  • errores que no se reproducen
  • bugs que desaparecen al poner logs
  • comportamientos distintos con la misma entrada

Y ahí es donde empieza el conflicto mental:

¿No se suponía que un programa era una función?
¿No debería ser determinista?

La respuesta corta es: no.

La respuesta interesante es: no puede serlo.


Un programa no vive en el vacío

Haskell te enseña a razonar como si el mundo fuese ideal:

  • sin tiempo
  • sin red
  • sin concurrencia
  • sin fallos
  • sin entorno

Pero un programa real no vive ahí.

Un programa real es:

un proceso físico ejecutándose en una máquina compartida, gobernado por un sistema operativo, interactuando con un mundo que cambia constantemente.

Eso rompe el determinismo por todos lados.


La red no falla: fallan las suposiciones

Uno de los primeros sitios donde esta idea se hace evidente es la red.

Decimos “ha fallado la red” como si fuese algo simple, pero en realidad la red es:

  • hardware
  • drivers
  • sistema operativo
  • protocolos
  • timeouts
  • lógica de aplicación

Un fallo de red puede acabar siendo simplemente:

  • una llamada que devuelve -1
  • una excepción
  • una conexión cerrada

Desde el punto de vista del programa, todo el caos externo se reduce a un valor de retorno.

El mundo puede estar ardiendo, pero tu código solo ve:

ERROR

¿Y las excepciones? ¿Son algo especial?

Aquí vuelve el choque con la intuición.

En los lenguajes de alto nivel hablamos de excepciones como si fuesen algo casi mágico, pero en realidad no lo son.

A bajo nivel:

  • no existen las excepciones
  • existen comparaciones
  • existen saltos
  • existe cambio de flujo de ejecución

Una excepción, ya sea:

  • un NullPointerException
  • un segmentation fault
  • un timeout

acaba siendo siempre lo mismo:

una transferencia no local del control de ejecución


El verdadero origen del caos: la concurrencia

Si Haskell te enseña el mundo ideal, la concurrencia te enseña por qué ese mundo no existe.

En cuanto hay:

  • varios hilos
  • varios procesos
  • interrupciones
  • planificación del sistema operativo

el orden deja de ser algo fijo.

Y sin orden fijo, no hay determinismo.

El mismo código puede ejecutarse en órdenes distintos, produciendo resultados distintos, sin que haya ningún “bug” evidente en el código.

El detalle que nadie te cuenta: el tiempo

Incluso aunque no compartas datos, el tiempo introduce incertidumbre:

  • cuándo se ejecuta un hilo
  • cuándo llega un paquete
  • cuándo expira un timeout
  • cuánto tarda una operación

Dos ejecuciones nunca ocurren en el mismo instante físico.

Por tanto, nunca son idénticas.

Entonces… ¿Haskell miente?

No. Haskell no miente. Haskell acota el problema.

Lo que hace es separar dos mundos:

  • el mundo puro, determinista, razonable
  • el mundo impuro, lleno de IO, tiempo y efectos

Y te obliga a reconocer explícitamente cuándo cruzas de uno a otro.

Eso, más que una limitación, es una lección de ingeniería brutal.

De la teoría a la ingeniería

Cuando entiendes todo esto, cambia tu forma de programar:

  • dejas de asumir que “esto no puede pasar”
  • dejas de confiar en el orden
  • empiezas a diseñar para el fallo

Por eso existen cosas como:

  • retries
  • timeouts explícitos
  • idempotencia
  • colas
  • circuit breakers
  • observabilidad

No porque seamos exagerados, sino porque el fallo no es una excepción, es un estado normal del sistema

Conclusión

Estudiar paradigmas como el funcional puro no te prepara directamente para escribir sistemas distribuidos, pero te da algo mucho más valioso:

un modelo mental claro de cómo deberían ser las cosas

Y entender por qué el mundo real no cumple ese modelo es lo que marca la diferencia entre escribir código que funciona “en mi máquina” y diseñar sistemas que sobreviven al caos.