map, andThen y withDefault: encadenar sin anidar
Una vez que la ausencia y el fallo son valores, aparece el problema práctico: cada operación insegura obliga a un case, y tres operaciones seguidas producen una pirámide ilegible en la que la lógica queda sepultada bajo la ceremonia. Esta lección desarrolla las tres herramientas que disuelven esa pirámide sin renunciar a ninguna garantía. Con map se transforma el contenido sin abrir la caja, porque la función que se aplica no sabe nada del contenedor. Con andThen se encadena una operación que a su vez puede fallar, evitando el anidamiento de contenedores y propagando el primer fracaso hasta el final. Con withDefault se sale del contenedor en el último momento, aportando el valor de reserva justo donde hay contexto para elegirlo. Se estudian además map2 para combinar independientes y mapError para adaptar la causa, y se muestra que las mismas cuatro funciones existen para Maybe y para Result porque comparten la misma estructura algebraica.
Hasta aquí la historia es virtuosa pero incómoda. Si cada operación que puede fracasar devuelve una caja, y solo se puede mirar dentro de una caja con un case, entonces un cálculo con cuatro pasos inseguros produce cuatro case anidados, cada uno con su rama de fallo repetida palabra por palabra, y la lógica que de verdad importa acaba enterrada bajo cuarenta líneas de ceremonia idéntica. Es una crítica justa, y la respuesta de Elm no consiste en aflojar la garantía sino en reconocer un patrón: en esa pirámide, la parte que cambia es minúscula y la parte que se repite es casi todo. Extraer lo repetido a un puñado de funciones deja el código a la altura de su intención. Tres nombres bastan para casi todo, y conviene aprenderlos como lo que son: no atajos para no escribir case, sino las tres preguntas distintas que se pueden hacer sobre un valor encajado. Transformar lo de dentro sin sacarlo es map; encadenar otro paso que a su vez puede fallar es andThen; salir de la caja aportando un valor de reserva es withDefault.
- Reconocer la pirámide de
caseanidados como un patrón repetido y no como una consecuencia inevitable de la seguridad. - Usar
mappara transformar el contenido de unMaybeo unResultcon funciones que ignoran por completo el contenedor. - Usar
andThenpara encadenar operaciones que devuelven contenedor, evitando el anidamiento y propagando el primer fallo. - Salir del contenedor con
withDefaulten el punto donde existe contexto para elegir el valor de reserva.
La pirámide y lo que se repite en ella
Empecemos por el problema en su forma más cruda. Tres conversiones seguidas, cada una capaz de no dar valor, escritas con la única herramienta que conocemos hasta ahora.
-- La piramide: la logica util cabe en una linea y el andamiaje ocupa quince
totalDeLaCesta : String -> String -> Maybe Int
totalDeLaCesta textoPrecio textoCantidad =
case String.toInt textoPrecio of
Nothing ->
Nothing
Just precio ->
case String.toInt textoCantidad of
Nothing ->
Nothing
Just cantidad ->
Just (precio * cantidad)
Mira qué parte del código es específica de este problema: la multiplicación de la última línea. Todo lo demás es un ritual mecánico que dice si hay valor sigo, si no hay valor me detengo, repetido tantas veces como pasos haya. Ese ritual es siempre idéntico, no depende del tipo del contenido ni de lo que quieras hacer con él, y por tanto es exactamente la clase de repetición que una función de orden superior puede absorber de una vez para siempre.
map: transformar sin abrir la caja
La primera pregunta es la más simple: tengo una caja que quizá contenga un valor y una función que sabe transformar ese valor, pero no sabe nada de cajas. Maybe.map las une. Si la caja tiene contenido, aplica la función dentro y vuelve a cerrar; si está vacía, no aplica nada y devuelve el vacío tal cual.
-- Maybe.map : (a -> b) -> Maybe a -> Maybe b
Maybe.map String.toUpper (Just "elm") -- Just "ELM"
Maybe.map String.toUpper Nothing -- Nothing
-- Result.map : (a -> b) -> Result e a -> Result e b
Result.map (\n -> n * 2) (Ok 21) -- Ok 42
Result.map (\n -> n * 2) (Err "invalido") -- Err "invalido"
Lo importante de map es lo que no exige. La función que le pasas es una función corriente sobre datos corrientes, escrita sin ninguna conciencia de que existan el fallo o la ausencia. Toda tu lógica de dominio puede seguir hablando de enteros y de cadenas, y solo el punto de unión sabe que hay un contenedor por medio. Esa separación es la que impide que la opcionalidad se filtre y contamine cada función del programa, que es justo lo que ocurre cuando la comprobación se hace a mano dentro de cada una.
En Result, map tiene además una asimetría deliberada: transforma el lado del éxito y deja intacto el del fallo. Para tocar el otro lado existe mapError, que adapta la causa sin mirar el valor, y que resulta indispensable cuando un error de una capa tiene que reexpresarse en el vocabulario de la capa que lo recibe.
andThen: encadenar pasos que también pueden fallar
La segunda pregunta es más profunda. ¿Y si la función que quieres aplicar no devuelve un valor limpio, sino otra caja? Con map obtendrías un contenedor dentro de otro contenedor, algo del estilo de un Maybe (Maybe Int), que es a la vez inútil y contagioso: cada paso añadiría una capa más. andThen existe precisamente para aplanar esa capa sobrante.
-- Maybe.andThen : (a -> Maybe b) -> Maybe a -> Maybe b
-- Result.andThen : (a -> Result e b) -> Result e a -> Result e b
totalDeLaCesta : String -> String -> Maybe Int
totalDeLaCesta textoPrecio textoCantidad =
String.toInt textoPrecio
|> Maybe.andThen
(\precio ->
String.toInt textoCantidad
|> Maybe.map (\cantidad -> precio * cantidad)
)
-- Cadena lineal: cada paso recibe el exito del anterior
edadDelPerfil : Dict String Usuario -> String -> Maybe Int
edadDelPerfil usuarios nombre =
Dict.get nombre usuarios
|> Maybe.andThen .perfil
|> Maybe.andThen (\perfil -> String.toInt perfil.edad)
|> Maybe.map (\edad -> edad + 1)
Esa segunda cadena es la que conviene mirar despacio, porque contiene toda la enseñanza del nivel. Tres operaciones que pueden fracasar, encadenadas en cuatro líneas planas, sin un solo case y sin haber perdido nada: si el usuario no está, el resultado es Nothing; si está pero no tiene perfil, el resultado es Nothing; si la edad no es numérica, el resultado es Nothing. El cortocircuito está incorporado en andThen, que solo llama a la siguiente función cuando recibe éxito y en caso contrario propaga el fallo intacto hasta el final de la tubería.
map
Para cuando la función que aplicas devuelve un valor normal. Transforma el contenido y conserva la forma del contenedor, sin que la función sepa que existe.
andThen
Para cuando la función que aplicas devuelve otro contenedor. Aplana el resultado y cortocircuita en cuanto aparece el primer Nothing o el primer Err.
withDefault
La salida. Aporta el valor de reserva y devuelve un dato desnudo, listo para la vista; úsalo lo más tarde posible en el recorrido.
map2 y mapError
Para combinar dos contenedores independientes en uno solo, y para reexpresar la causa del fallo en el vocabulario de otra capa.
La regla práctica es mecánica y no falla. Si la función que quieres aplicar tiene la forma a -> b, usas map. Si tiene la forma a -> Maybe b o a -> Result e b, usas andThen. Cuando dudes, deja que el compilador decida: escribe map, y si el resultado te sale como un contenedor dentro de otro contenedor, esa capa de más es la señal inequívoca de que tocaba andThen. El mensaje de error de Elm te muestra literalmente el tipo anidado, así que el diagnóstico y el arreglo caben en el mismo segundo.
withDefault y el arte de salir tarde
La tercera pregunta es cuándo abandonar el contenedor. withDefault toma un valor de reserva y devuelve un dato desnudo, cerrando la conversación. Es la operación más tentadora y la que más conviene retrasar, porque en el instante en que la usas destruyes la distinción entre no había dato y el dato era justamente ese, y esa distinción a veces vale mucho.
Maybe.withDefault 0 (Just 42) -- 42
Maybe.withDefault 0 Nothing -- 0
Result.withDefault "sin nombre" (Err e) -- "sin nombre"
-- Combinar dos contenedores independientes sin encadenarlos
Maybe.map2 (\a b -> a ++ " " ++ b) (Just "Evan") (Just "Czaplicki")
El criterio de diseño es propagar el contenedor hacia arriba mientras la decisión no esté clara y resolverlo abajo del todo, en la vista, donde por fin sabes si un precio ausente se pinta como un guion, como un cero o como un aviso en rojo. Resolverlo pronto, en una función profunda del cálculo, equivale a tomar una decisión de interfaz en un sitio donde no tienes ninguna información para tomarla, y suele reaparecer como un cero misterioso en pantalla que nadie sabe de dónde salió.
flowchart LR A[Texto crudo] --> B[andThen paso uno] B -->|exito| C[andThen paso dos] B -->|fallo| F[Fallo propagado] C -->|exito| D[map transforma] C -->|fallo| F D --> W[withDefault en la vista] F --> W style B fill:#89b4fa,color:#11111b style D fill:#a6e3a1,color:#11111b style F fill:#f38ba8,color:#11111b
Conviene decir en voz alta lo que estas tres funciones son en realidad, porque Elm lo oculta a propósito y entenderlo cambia la forma de leer todo lo demás. map, andThen y el constructor Just u Ok forman, juntos, una estructura conocida y muy estudiada: un funtor y una mónada. map es la operación que hace de Maybe un funtor, es decir, un contenedor que sabe aplicar funciones a su contenido sin alterar su forma. El constructor mete un valor limpio dentro del contexto mínimo. Y andThen es la operación de enlace que permite secuenciar cálculos contextuales, ese ligamento que hace mónada al conjunto y que obedece a las mismas leyes en todos los casos: enlazar con el constructor no cambia nada, enlazar un constructor con una función equivale a aplicar la función, y el orden de agrupación de una cadena de enlaces es irrelevante. Que esas leyes se cumplan no es trivia académica, es la garantía de que puedes refactorizar una tubería —partirla en dos, extraer un tramo a una función auxiliar, reordenar los paréntesis— sin cambiar su significado. Y explica de golpe por qué Maybe, Result, List, Task, Decoder y Generator tienen todos un map y un andThen con la misma forma: no es una coincidencia de nomenclatura, es la misma estructura instanciada en contextos distintos, y por eso aprender el patrón una vez sirve para toda la biblioteca. Ahora bien, Elm hace aquí una elección muy suya: al no tener clases de tipos, no puede escribir un map genérico que valga para cualquier contenedor, así que ofrece Maybe.map, Result.map, List.map y Task.map como funciones separadas, y no tiene notación especial que disimule las cadenas de enlace. Se paga en repetición y en verbosidad. Lo que se compra a cambio es que no existe abstracción invisible: un principiante puede leer Maybe.andThen y comprender exactamente qué hace mirando su tipo, sin necesitar una jerarquía de conceptos matemáticos previos. Es la misma decisión que gobierna el lenguaje entero, aplicada aquí a su rincón más teórico: preferir cien funciones obvias a una abstracción elegante que hay que estudiar antes de usar.
- Escribe
totalDeLaCestaconcaseanidados y luego reescríbela conandThenymap; cuenta las líneas de cada versión y señala cuáles son lógica y cuáles ceremonia. - Aplica
Maybe.mapcon una función que ignore por completo el contenedor y explica por qué esa ignorancia es una ventaja de diseño y no una carencia. - Provoca a propósito un
Maybe (Maybe Int)usandomapdonde tocabaandThen; lee el tipo del error y describe qué te está diciendo. - Construye una cadena de tres pasos con
Result.andTheny comprueba que el primerErrcortocircuita el resto sin ejecutar los pasos siguientes. - Usa
mapErrorpara traducir el error de una capa al vocabulario de otra y razona por qué esa traducción no debería hacerse automáticamente. - Localiza en un proyecto un
withDefaultprematuro, súbelo hasta la vista y argumenta qué información se recupera al retrasarlo.