Capítulo 09: Cebollas Mónadicas
Fábrica de Functores Señalados
Antes de profundizar, debo hacer una confesión: no he sido totalmente honesto sobre el método of que colocamos en cada tipo. En realidad, no existe para evitar la palabra clave new, sino para ubicar valores en lo que se denomina contexto mínimo predeterminado. Sí, of no reemplaza un constructor: es parte de una interfaz importante llamada Pointed.
Un functor señalado es un functor con un método
of
Lo crucial aquí es la capacidad de inyectar cualquier valor en nuestro tipo y comenzar a aplicar map.
IO.of('tetris').map(concat(' master'));
// IO('tetris master')
Maybe.of(1336).map(add(1));
// Maybe(1337)
Task.of([{ id: 2 }, { id: 3 }]).map(map(prop('id')));
// Task([2,3])
Either.of('The past, present and future walk into a bar...').map(concat('it was tense.'));
// Right('The past, present and future walk into a bar...it was tense.')Si recuerdan, los constructores de IO y Task requieren una función como argumento, pero Maybe y Either no. La motivación para esta interfaz es proveer una forma común de insertar valores en nuestro functor sin las complejidades específicas de los constructores. El término "contexto mínimo predeterminado" es impreciso, pero capta bien la idea: queremos inyectar valores en el tipo y aplicar map según el comportamiento esperado del functor.
Una corrección importante: Left.of carece de sentido. En Either, el método válido es new Right(x). Definimos of con Right porque si el tipo puede aplicar map, debe hacerlo. Left rompe este patrón.
Habrán oído términos como pure, point, unit y return: son equivalentes de of. Este método será clave con las mónadas, pues debemos reinsertar valores manualmente en el tipo.
Para evitar new, usemos librerías como folktale, ramda o fantasy-land que proveen implementaciones correctas de of.
Mezclando Metáforas

Las mónadas son como cebollas. Veamos un ejemplo común:
const fs = require('fs');
// readFile :: String -> IO String
const readFile = filename => new IO(() => fs.readFileSync(filename, 'utf-8'));
// print :: String -> IO String
const print = x => new IO(() => {
console.log(x);
return x;
});
// cat :: String -> IO (IO String)
const cat = compose(map(print), readFile);
cat('.git/config');
// IO(IO('[core]\nrepositoryformatversion = 0\n'))Aquí tenemos un IO anidado dentro de otro por el map de print. Para trabajar con el string, debemos anidar maps y desencapsular dos veces con unsafePerformIO.
// cat :: String -> IO (IO String)
const cat = compose(map(print), readFile);
// catFirstChar :: String -> IO (IO String)
const catFirstChar = compose(map(map(head)), cat);
catFirstChar('.git/config');
// IO(IO('['))Aunque tener efectos encapsulados es útil, la API resultante es engorrosa como trabajar con capas redundantes. Observemos otro caso:
// safeProp :: Key -> {Key: a} -> Maybe a
const safeProp = curry((x, obj) => Maybe.of(obj[x]));
// safeHead :: [a] -> Maybe a
const safeHead = safeProp(0);
// firstAddressStreet :: User -> Maybe (Maybe (Maybe Street))
const firstAddressStreet = compose(
map(map(safeProp('street'))),
map(safeHead),
safeProp('addresses'),
);
firstAddressStreet({
addresses: [{ street: { name: 'Mulburry', number: 8402 }, postcode: 'WC2N' }],
});
// Maybe(Maybe(Maybe({name: 'Mulburry', number: 8402})))El anidamiento de tres functors exige múltiples maps al usuario. Este patrón recurrente es donde las mónadas brillan.
Decimos que las mónadas son cebollas porque al separar capas con map lagrimeamos. Pero podemos usar join para fusionar capas.
const mmo = Maybe.of(Maybe.of('nunchucks'));
// Maybe(Maybe('nunchucks'))
mmo.join();
// Maybe('nunchucks')
const ioio = IO.of(IO.of('pizza'));
// IO(IO('pizza'))
ioio.join();
// IO('pizza')
const ttt = Task.of(Task.of(Task.of('sewers')));
// Task(Task(Task('sewers')));
ttt.join();
// Task(Task('sewers'))Si hay dos capas del mismo tipo, las fusionamos con join. Esta capacidad de unión define una mónada.
Las mónadas son functores señalados que pueden fusionarse
Todo functor con método join, of y que cumple ciertas leyes es mónada. Implementemos join para Maybe:
Maybe.prototype.join = function join() {
return this.isNothing() ? Maybe.of(null) : this.$value;
};Simple: si tenemos Maybe(Maybe(x)), join remueve una capa. Si es Nothing, nada cambia.
Apliquemos join al ejemplo firstAddressStreet:
// join :: Monad m => m (m a) -> m a
const join = mma => mma.join();
// firstAddressStreet :: User -> Maybe Street
const firstAddressStreet = compose(
join,
map(safeProp('street')),
join,
map(safeHead), safeProp('addresses'),
);
firstAddressStreet({
addresses: [{ street: { name: 'Mulburry', number: 8402 }, postcode: 'WC2N' }],
});
// Maybe({name: 'Mulburry', number: 8402})Invocamos join donde hubo anidamientos de Maybe. Hagamos lo mismo con IO:
IO.prototype.join = function() {
const $ = this;
return new IO(() => $.unsafePerformIO().unsafePerformIO());
};Ejecutamos secuencialmente las dos capas de IO sin perder pureza. Simplificamos el manejo al fusionar capas.
// log :: a -> IO a
const log = x => new IO(() => {
console.log(x);
return x;
});
// setStyle :: Selector -> CSSProps -> IO DOM
const setStyle =
curry((sel, props) => new IO(() => jQuery(sel).css(props)));
// getItem :: String -> IO String
const getItem = key => new IO(() => localStorage.getItem(key));
// applyPreferences :: String -> IO DOM
const applyPreferences = compose(
join,
map(setStyle('#main')),
join,
map(log),
map(JSON.parse),
getItem,
);
applyPreferences('preferences').unsafePerformIO();
// Object {backgroundColor: "green"}
// <div style="background-color: 'green'"/>getItem devuelve IO String, aplicamos map para parsearlo. log y setStyle devuelven IO: usamos join para controlar el anidamiento.
Mi Cadena Golpea Mi Pecho

El patrón común de map seguido de join lo combinamos en una sola función llamada chain (alias flatMap o >>=).
// chain :: Monad m => (a -> m b) -> m a -> m b
const chain = curry((f, m) => m.map(f).join());
// or
// chain :: Monad m => (a -> m b) -> m a -> m b
const chain = f => compose(join, map(f));Refactoricemos los ejemplos usando chain:
// map/join
const firstAddressStreet = compose(
join,
map(safeProp('street')),
join,
map(safeHead),
safeProp('addresses'),
);
// chain
const firstAddressStreet = compose(
chain(safeProp('street')),
chain(safeHead),
safeProp('addresses'),
);
// map/join
const applyPreferences = compose(
join,
map(setStyle('#main')),
join,
map(log),
map(JSON.parse),
getItem,
);
// chain
const applyPreferences = compose(
chain(setStyle('#main')),
chain(log),
map(JSON.parse),
getItem,
);chain no solo limpia código: permite secuenciar efectos y manejar asignaciones funcionales fluidamente mediante encadenamiento.
// getJSON :: Url -> Params -> Task JSON
getJSON('/authenticate', { username: 'stale', password: 'crackers' })
.chain(user => getJSON('/friends', { user_id: user.id }));
// Task([{name: 'Seimith', id: 14}, {name: 'Ric', id: 39}]);
// querySelector :: Selector -> IO DOM
querySelector('input.username')
.chain(({ value: uname }) =>
querySelector('input.email')
.chain(({ value: email }) => IO.of(`Welcome ${uname} prepare for spam at ${email}`))
);
// IO('Welcome Olivia prepare for spam at olivia@tremorcontrol.net');
Maybe.of(3)
.chain(three => Maybe.of(2).map(add(three)));
// Maybe(5);
Maybe.of(null)
.chain(safeProp('address'))
.chain(safeProp('street'));
// Maybe(null);Se podría usar compose, pero chain permite estilo infijo. Técnicamente, chain(f) equivale a map(f).join(). También join puede definirse como chain(id). Estas relaciones están especificadas en fantasyland.
En los ejemplos, chain secuencia Tasks asíncronas sin anidamiento innecesario.
Al usar querySelector, chain permite acceder variables en clausuras. IO.of mantiene el contexto como requiere la interfaz Pointed.
querySelector('input.username').chain(({ value: uname }) =>
querySelector('input.email').map(({ value: email }) =>
`Welcome ${uname} prepare for spam at ${email}`));
// IO('Welcome Olivia prepare for spam at olivia@tremorcontrol.net');Con Maybe, si aparece null, chain detiene la computación inmediatamente.
No desesperen si los ejemplos son confusos al inicio. Experimenten. Recuerden: usar map para valores normales y chain cuando retornen functors.
Nota: esto no funciona con tipos anidados distintos. La composición de functores y transformadores mónada ayudan allí.
Ejercicio de Poder
Programar con contenedores puede ser complejo. A veces cuesta saber cuántas capas hay o si usar map/chain. Mejoramos la depuración con inspect, pero a veces dudamos si vale la pena.
Demostremos un ejemplo potente:
Leer un archivo y luego subirlo:
// readFile :: Filename -> Either String (Task Error String)
// httpPost :: String -> String -> Task Error JSON
// upload :: Filename -> Either String (Task Error JSON)
const upload = compose(map(chain(httpPost('/uploads'))), readFile);Aquí manejamos tres errores potenciales (validación de entrada, acceso a archivo, error de red) de forma declarativa con Either y Task, encadenando acciones asíncronas limpiamente.
Todo se logra mediante un flujo lineal, puro y declarativo. Mantiene razonamiento ecuacional y propiedades confiables. Contrastemos con el enfoque imperativo:
El enfoque imperativo implica anidamiento caótico de callbacks, propenso a errores. ¡Imaginen con mutaciones de variables!
// upload :: Filename -> (String -> a) -> Void
const upload = (filename, callback) => {
if (!filename) {
throw new Error('You need a filename!');
} else {
readFile(filename, (errF, contents) => {
if (errF) throw errF;
httpPost('/uploads', contents, (errH, json) => {
if (errH) throw errH;
callback(json);
});
});
}
};La versión imperativa es como una bola en un laberinto volátil.
Teoría
Primera ley: asociatividad en la fusión de mónadas.
// associativity
compose(join, map(join)) === compose(join, join);Al fusionar M(M(M a)), el orden de aplicar join sobre capas internas/externas no afecta el resultado final.

Ya sea fusionando primero capas externas o internas, el último join produce el mismo M a. Nótese que map(join) difiere de join, pero el resultado final coincide.
Segunda ley: identidad
// identity for all (M a)
compose(join, of) === compose(join, map(of)) === id;Para toda mónada M, join(of(M a)) = M a. Visualmente forma un triángulo: aplicar of y luego join equivale a id.

Inyectar y luego fusionar retorna al estado original, independiente de aplicar of internamente mediante map.
Recuerden: debemos usar of específico del tipo de mónada.
Estas leyes corresponden a una categoría en teoría de categorías. Las mónadas forman la categoría de Kleisli.
const mcompose = (f, g) => compose(chain(f), g);
// left identity
mcompose(M, f) === f;
// right identity
mcompose(f, M) === f;
// associativity
mcompose(mcompose(f, g), h) === mcompose(f, mcompose(g, h));El objetivo es mostrar aplicaciones prácticas. Conocer estas bases ayuda a aprovechar mejor las mónadas en el trabajo diario.
En Resumen
Las mónadas permiten operar en cómputos anidados sin pirámides de callback. Secuencian efectos asíncronos manteniendo pureza. Usando chain y of, manejamos valores encapsulados eficientemente.
Pese a su potencia, hay casos que requieren otros patrones: ejecución paralela o validaciones acumulativas. Ahí entran los functores aplicativos.
En el próximo capítulo exploraremos functores aplicativos y su utilidad frente a mónadas.