wandres.dev
MAYBE Y RESULT · sin excepciones

Maybe: la ausencia como un valor que el tipo declara

Elm no tiene null ni undefined, y esa carencia es deliberada: en su lugar define un tipo de unión corriente, Maybe, con dos constructores —Just, que envuelve un valor presente, y Nothing, que nombra la ausencia—. Esta lección desarrolla por qué trasladar la ausencia desde el terreno invisible de las referencias hasta el terreno visible de los tipos elimina de raíz la clase de errores más cara de la historia del software. Se estudia Maybe como tipo paramétrico, la diferencia decisiva entre un String que además podría ser nulo y un Maybe String que declara su opcionalidad en la propia firma, la imposibilidad de tocar el contenido sin desempaquetarlo antes, y la exhaustividad del case que obliga a escribir la rama vacía. La idea de fondo es que la ausencia deja de ser un accidente que puede ocurrirle a cualquier valor en cualquier momento y pasa a ser una propiedad declarada del tipo, verificada por el compilador antes de que el programa llegue a ejecutarse.

⏱ 18 min

En 1965 Tony Hoare introdujo la referencia nula en ALGOL W por una razón que él mismo calificó de irresistible: era muy fácil de implementar. Cuarenta años después la llamó su error de mil millones de dólares, y la cifra se ha quedado corta con holgura. El problema nunca fue la ausencia en sí —los datos faltan constantemente y eso es perfectamente legítimo—, sino el modo en que null la representa: como un valor fantasma capaz de habitar cualquier tipo sin que el tipo lo mencione. Una variable declarada como cadena promete ser una cadena y, sin embargo, puede no serlo; la promesa se rompe en silencio y solo se descubre al desreferenciarla, en tiempo de ejecución, con el programa ya delante del usuario. Elm elimina el fantasma por el procedimiento más simple concebible: no existe null, no existe undefined, no hay ningún comodín capaz de ocupar el lugar de otro valor. Cuando la ausencia es posible, se declara con un tipo que la nombra, Maybe, y desde ese instante deja de ser un accidente para convertirse en información que el compilador lee, propaga y te exige tratar.

🎯 Al terminar esta lección sabrás
  • Entender por qué null es un defecto del sistema de tipos y no una falta de disciplina del programador.
  • Leer la definición de Maybe como una unión etiquetada ordinaria con dos constructores, Just y Nothing.
  • Distinguir en la firma de una función un valor garantizado de uno opcional, antes de ejecutar una sola línea.
  • Desempaquetar un Maybe con case y comprobar que la exhaustividad obliga a decidir qué ocurre en la ausencia.

La ausencia como constructor, no como agujero

Lo primero que sorprende de Maybe es que no es una construcción especial del lenguaje. No hay sintaxis dedicada, ni operadores mágicos, ni tratamiento privilegiado en el compilador. Es un tipo de unión perfectamente corriente, del mismo material con el que tú defines los tuyos, y podrías escribirlo en tres líneas si la biblioteca estándar no lo trajera ya hecho.

-- Maybe no es magia del lenguaje: es una union etiquetada corriente
type Maybe a
    = Just a
    | Nothing


-- La firma confiesa la posibilidad de ausencia antes de leer el cuerpo
buscarUsuario : Int -> List Usuario -> Maybe Usuario
buscarUsuario id usuarios =
    case List.filter (\u -> u.id == id) usuarios of
        encontrado :: _ ->
            Just encontrado

        [] ->
            Nothing

La a de la definición es una variable de tipo: Maybe es paramétrico y funciona igual para Maybe Int, Maybe String o Maybe Usuario. Los dos constructores son las dos únicas formas de fabricar un valor de ese tipo y, por tanto, las dos únicas formas en que puede llegarte. Just es una caja con algo dentro; Nothing es una caja vacía. Ninguna de las dos es un puntero inválido: ambas son datos legítimos, construidos, comparables e inspeccionables.

Compara ahora dos firmas. nombre : String promete, sin condiciones ni notas al pie, que ahí hay una cadena. nombre : Maybe String promete otra cosa distinta: que ahí hay una caja que puede contener una cadena o estar vacía. En la mayoría de los lenguajes esas dos firmas se escriben igual, y la diferencia entre ellas vive en la documentación, en el nombre de la variable o en la memoria de quien escribió el código hace dos años. En Elm la diferencia está en el tipo, que es la única parte del programa que nunca miente y que el compilador verifica siempre.

Lo que el tipo te obliga a hacer

Declarar la ausencia sería un gesto vacío si pudieras ignorarla. La fuerza de Maybe no está en informar, sino en impedir: un Maybe String no es un String, así que no puedes concatenarlo, ni medir su longitud, ni pasarlo a una función que espere una cadena. El sistema de tipos rechaza el programa entero. Para llegar al contenido hay que abrir la caja, y abrir la caja significa contemplar las dos formas en que puede venir.

saludar : Maybe String -> String
saludar posibleNombre =
    case posibleNombre of
        Just nombre ->
            "Hola, " ++ nombre

        Nothing ->
            "Hola, visitante"

Si borras la rama Nothing, el compilador no emite un aviso archivable: falla la compilación y te dice exactamente qué caso has dejado sin cubrir. Esa es la inversión decisiva respecto a null. Con null, olvidar el caso vacío produce un programa que compila, se despliega y revienta cuando un usuario real tropieza con el dato que faltaba. Con Maybe, olvidar el caso vacío produce un programa que sencillamente no llega a existir.

📦

Just

El constructor de la presencia. Envuelve un valor cualquiera y lo marca como disponible, pero sigue siendo una caja: hay que abrirla para usar lo que contiene.

🕳️

Nothing

El constructor de la ausencia. No es un error ni una referencia inválida: es un dato completo y legítimo que significa aquí no hay nada, y se puede guardar, comparar y devolver.

🔍

case exhaustivo

La única puerta de entrada al contenido. El compilador verifica que has cubierto ambos constructores y rechaza el programa si dejas uno fuera.

🧭

La firma como aviso

Ver Maybe en un tipo es saber, sin leer una sola línea de implementación, que ese valor puede faltar y que tú tendrás que decidir qué ocurre entonces.

⚠️
No es null con otro nombre

La objeción habitual es que Maybe se limita a renombrar el problema. No es así, y la diferencia es estructural. null es un valor que pertenece a todos los tipos a la vez: cualquier referencia puede serlo, de modo que la comprobación haría falta en todas partes y nada te recuerda dónde. Maybe a es un tipo distinto de a: solo los valores declarados explícitamente como opcionales pueden faltar y, en esos —y solo en esos—, el compilador te detiene. Con null la posición por defecto es la desconfianza universal; con Maybe, la confianza salvo declaración expresa. Además Maybe compone: puedes tener una lista de opcionales, un opcional dentro de un resultado o un campo opcional dentro de un registro, y cada nivel sigue siendo visible en el tipo en lugar de fundirse con los demás.

Nothing no es un error: es una respuesta legítima

Conviene desactivar una lectura ansiosa de Maybe. Nothing no señala que algo haya ido mal; señala que la respuesta correcta a la pregunta formulada es que no hay valor. Buscar un usuario que no está en la lista no es un fallo del programa: es un resultado previsto. Por eso la biblioteca estándar devuelve Maybe en todas las operaciones que pueden no dar fruto, y lo hace sin dramatismo alguno.

-- Firmas de la biblioteca estandar que declaran su parcialidad
-- List.head : List a -> Maybe a
-- String.toInt : String -> Maybe Int
-- Dict.get : comparable -> Dict comparable v -> Maybe v
-- Array.get : Int -> Array a -> Maybe a


ejemplos : List (Maybe Int)
ejemplos =
    [ List.head []              -- Nothing
    , List.head [ 1, 2, 3 ]     -- Just 1
    , String.toInt "42"         -- Just 42
    , String.toInt "cuarenta"   -- Nothing
    ]

En un lenguaje con excepciones, una conversión de texto a número lanzaría un error ante una entrada no numérica y pedir la cabeza de una lista vacía reventaría el programa. Elm convierte esas funciones parciales —definidas solo para algunas entradas— en funciones totales que devuelven algo sensato para todas: un Maybe. La totalidad es la propiedad que sostiene la garantía entera del lenguaje, y Maybe es la herramienta más humilde y más usada para conseguirla.

flowchart LR
E[Entrada cualquiera] --> F[Funcion total]
F --> J[Just valor]
F --> N[Nothing]
J --> C[case obligatorio]
N --> C
C --> S[Programa que compila]
style F fill:#89b4fa,color:#11111b
style C fill:#a6e3a1,color:#11111b

Hay una consecuencia práctica que se nota enseguida: los Maybe tienden a viajar hacia arriba. Si una función interna puede no encontrar el dato, su tipo lo dice, y quien la llame heredará esa opcionalidad salvo que decida resolverla ahí mismo. Eso empuja a tomar la decisión —qué mostrar, qué valor por defecto usar, qué rama seguir— en el lugar donde de verdad hay contexto para tomarla, normalmente cerca de la vista, en vez de enterrarla en un rincón del cálculo donde nadie sabrá que se tomó.

Cuándo Maybe no es la herramienta adecuada

Como toda buena idea, Maybe se sobreutiliza en cuanto se descubre, y el síntoma más claro aparece en el modelo. Un campo opcional dice que puede haber valor o no haberlo, pero no dice por qué falta, y en un modelo esa diferencia suele ser justamente la información que la vista necesita. Un dato que aún no se ha pedido, uno que se está cargando, uno que falló y uno que llegó vacío son cuatro situaciones distintas que un solo Maybe aplasta en dos.

-- Modelo pobre: dos combinaciones imposibles caben en estos tres campos
type alias ModeloPobre =
    { cargando : Bool
    , datos : Maybe (List Chiste)
    , error : Maybe String
    }


-- Modelo honesto: cada estado es un constructor y ninguno se solapa
type EstadoRemoto
    = NoSolicitado
    | Cargando
    | Fallido String
    | Cargado (List Chiste)

El primer modelo admite estados que no significan nada: cargando y con error a la vez, o con datos y con error simultáneamente. El segundo hace irrepresentables esas combinaciones, y la vista que lo consume es un case de cuatro ramas sin condicionales cruzados. La regla que se deduce es sencilla: usa Maybe cuando la ausencia se explica sola y no admite matices, y define un tipo propio en cuanto empieces a necesitar saber por qué no hay valor. Peor todavía es el Maybe Bool, que ofrece tres estados donde casi siempre querías nombrar tres cosas distintas; si te aparece uno, casi con seguridad hay un tipo de tres constructores esperando a ser escrito.

Mover la ausencia del mundo de los valores al mundo de los tipos

El fondo del asunto es que null intenta resolver en el nivel de los valores un problema que solo tiene solución en el nivel de los tipos. Un sistema de tipos es, en última instancia, una maquinaria de demostración: cuando afirma que una expresión es una cadena, está probando que en ningún camino de ejecución podrá ser otra cosa. null rompe esa demostración desde dentro, porque introduce un habitante clandestino en absolutamente todos los tipos de referencia. El tipo sigue escrito, pero ha dejado de ser un teorema; se ha convertido en una sugerencia. Y como el compilador ya no puede probar nada sobre la presencia del dato, la carga de la prueba recae íntegra sobre el programador, que debe recordar comprobar en cada uso, sin ninguna ayuda para saber cuáles de los miles de usos lo necesitan. La corrección pasa a depender de la memoria humana, que es exactamente el recurso menos fiable del sistema. Maybe hace la jugada contraria: en lugar de debilitar todos los tipos para acomodar la ausencia, crea un tipo nuevo que la contiene. Lo que antes era una excepción invisible a la regla se convierte en una regla más, escrita en el mismo lenguaje que todo lo demás y verificable con las mismas herramientas. La opcionalidad deja de ser una propiedad global del universo y pasa a ser un dato local que se declara donde ocurre, se propaga por las firmas y se resuelve donde hay información para resolverlo. Este movimiento tiene nombre en el diseño de software —hacer irrepresentables los estados ilegales— y Maybe es su instancia más elemental: no puedes escribir un programa que use un valor ausente, no porque te prohíban hacerlo, sino porque ese programa no es expresable. La disciplina se sustituye por imposibilidad, y esa sustitución es lo que separa un lenguaje que te avisa de uno que te garantiza.

⚔️ Haz visible la ausencia en tus tipos
  1. Escribe type Maybe a = Just a | Nothing a mano con otro nombre y comprueba que obtienes exactamente el mismo comportamiento: confirma que no hay magia en el compilador.
  2. Define buscarPorId : Int -> List Usuario -> Maybe Usuario y prueba que devuelve Nothing con una lista vacía sin lanzar ningún error.
  3. Intenta concatenar directamente un Maybe String con otra cadena; lee el mensaje del compilador y explica con precisión qué invariante está protegiendo.
  4. Borra la rama Nothing de un case y observa que el fallo llega en compilación; contrasta ese momento con el instante en que habría llegado con una referencia nula.
  5. Recorre tu modelo y separa los campos que siempre tienen valor de los que pueden no tenerlo; declara solo estos últimos como Maybe y razona qué has ganado en los otros.
  6. Argumenta por qué Nothing devuelto por una búsqueda no es un error del programa y qué implicaciones tiene esa distinción para el diseño de la vista.