Haskell, determinismo y el choque con el mundo real
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.