Capítulo 08: Tupperware
El Poderoso Contenedor

Hemos visto cómo escribir programas que canalizan datos a través de una serie de funciones puras. Estas son especificaciones declarativas de comportamiento. Pero ¿qué pasa con el flujo de control, manejo de errores, acciones asíncronas, estado y, me atrevo a decir, efectos?! En este capítulo, descubriremos los fundamentos sobre los que se construyen todas estas útiles abstracciones.
Primero crearemos un contenedor. Este debe contener cualquier tipo de valor; una bolsa con cierre hermético que solo guarde pudín de tapioca sería poco útil. Será un objeto, pero no le daremos propiedades ni métodos en el sentido de la programación orientada a objetos. Lo trataremos como un cofre del tesoro: una caja especial que protege nuestros valiosos datos.
class Container {
constructor(x) {
this.$value = x;
}
static of(x) {
return new Container(x);
}
}Aquí está nuestro primer contenedor. Lo nombramos cuidadosamente Container. Usaremos Container.of como constructor para evitar escribir la palabreja new repetidamente. La función of tiene más profundidad de lo aparente, pero por ahora considérela como la forma correcta de almacenar valores en el contenedor.
Analicemos nuestra recién creada caja...
Container.of(3);
// Container(3)
Container.of('hotdogs');
// Container("hotdogs")
Container.of(Container.of({ name: 'yoda' }));
// Container(Container({ name: 'yoda' }))En Node verás {$value: x} aunque tengamos un Container(x). Chrome muestra el tipo correctamente, pero no importa: mientras entendamos la estructura de Container, todo está bien. En algunos entornos puedes sobrescribir el método inspect, pero no seremos tan exhaustivos. Para este libro, escribiremos salidas conceptuales como si hubiéramos modificado inspect, por razones pedagógicas y estéticas.
Aclaremos algunos puntos antes de continuar:
Containeres un objeto con una propiedad. Aunque muchos contenedores guardan un solo elemento, no es limitante. Arbitrariamente nombramos su propiedad$value.El
$valueno puede ser de un tipo específico, o nuestroContainerno cumpliría su propósito.Una vez dentro del
Container, los datos permanecen allí. Podríamos extraerlos con.$value, pero eso invalidaría su razón de ser.
Las razones detrás de esto se volverán claras como un frasco de vidrio, pero por ahora, tengan paciencia.
Mi Primer Functor
Con nuestro valor dentro del contenedor, necesitamos una forma de aplicar funciones.
// (a -> b) -> Container a -> Container b
Container.prototype.map = function (f) {
return Container.of(f(this.$value));
};Es similar al famoso map de los arrays, pero aquí tenemos Container a en lugar de [a]. Su funcionamiento es esencialmente igual:
Container.of(2).map(two => two + 2);
// Container(4)
Container.of('flamethrowers').map(s => s.toUpperCase());
// Container('FLAMETHROWERS')
Container.of('bombs').map(append(' away')).map(prop('length'));
// Container(10)Podemos manipular el valor sin salir nunca del Container. Esto es notable. El valor se pasa a map para su transformación y luego vuelve al Container para resguardo. Al no abandonar nunca el contenedor, podemos encadenar múltiples map y cambiar incluso su tipo, como muestran los ejemplos.
Espere un momento: si llamamos map repetidamente, ¡parece una forma de composición! ¿Qué magia matemática opera aquí? Amigos, acabamos de descubrir los Functors.
Un Functor es un tipo que implementa
mapy obedece ciertas leyes
Sí, Functor es simplemente una interfaz con un contrato. Podríamos haberlo llamado Mappable, pero ¿dónde quedaría la diversión? Los functores vienen de la teoría de categorías. Analizaremos las matemáticas más adelante, pero por ahora trabajemos en la intuición y usos prácticos.
¿Por qué encapsular valores y usar map para acceder? La respuesta viene al reformular: ¿Qué ganamos delegando la aplicación de funciones al contenedor? La abstracción de la aplicación de funciones. Cuando mapeamos, pedimos al contenedor que ejecute por nosotros. Un concepto poderoso.
El Maybe de Schrödinger

Container es bastante aburrido. Usualmente se llama Identity y tiene el mismo impacto que nuestra función id (existe una conexión matemática que veremos después). Sin embargo, hay otros functores con comportamientos útiles al mapear. Definamos uno ahora.
Ver implementación completa en Apéndice B
class Maybe {
static of(x) {
return new Maybe(x);
}
get isNothing() {
return this.$value === null || this.$value === undefined;
}
constructor(x) {
this.$value = x;
}
map(fn) {
return this.isNothing ? this : Maybe.of(fn(this.$value));
}
inspect() {
return this.isNothing ? 'Nothing' : `Just(${inspect(this.$value)})`;
}
}Maybe se parece a Container, pero primero verifica si tiene valor antes de aplicar funciones. Esto evita los molestos null durante el mapeo (nota: esta implementación está simplificada para enseñanza).
Maybe.of('Malkovich Malkovich').map(match(/a/ig));
// Just(True)
Maybe.of(null).map(match(/a/ig));
// Nothing
Maybe.of({ name: 'Boris' }).map(prop('age')).map(add(10));
// Nothing
Maybe.of({ name: 'Dinah', age: 14 }).map(prop('age')).map(add(10));
// Just(24)Nuestra aplicación no explota con errores al mapear valores nulos porque Maybe verifica sistemáticamente la presencia de valores.
La sintaxis de punto es válida, pero preferimos mantener estilo pointfree. map puede delegar a cualquier functor receptor:
// map :: Functor f => (a -> b) -> f a -> f b
const map = curry((f, anyFunctor) => anyFunctor.map(f));Esto permite continuar con composiciones normales. La notación Functor f => en firmas de tipo indica que f debe ser un Functor.
Casos de Uso
En proyectos reales, Maybe suele usarse en funciones con posible fallo:
// safeHead :: [a] -> Maybe(a)
const safeHead = xs => Maybe.of(xs[0]);
// streetName :: Object -> Maybe String
const streetName = compose(map(prop('street')), safeHead, prop('addresses'));
streetName({ addresses: [] });
// Nothing
streetName({ addresses: [{ street: 'Shady Ln.', number: 4201 }] });
// Just('Shady Ln.')safeHead es como head común pero con seguridad adicional. Al usar Maybe, debemos manejar valores nulos explícitamente. La función devuelve Maybe para indicar posible fallo, forzándonos a usar map para acceder al valor. Esto implementa verificaciones de nulidad seguras, garantizando software más robusto.
Algunas funciones devuelven Nothing explícitamente para señalar error:
// withdraw :: Number -> Account -> Maybe(Account)
const withdraw = curry((amount, { balance }) =>
Maybe.of(balance >= amount ? { balance: balance - amount } : null));
// This function is hypothetical, not implemented here... nor anywhere else.
// updateLedger :: Account -> Account
const updateLedger = account => account;
// remainingBalance :: Account -> String
const remainingBalance = ({ balance }) => `Your balance is $${balance}`;
// finishTransaction :: Account -> String
const finishTransaction = compose(remainingBalance, updateLedger);
// getTwenty :: Account -> Maybe(String)
const getTwenty = compose(map(finishTransaction), withdraw(20));
getTwenty({ balance: 200.00 });
// Just('Your balance is $180')
getTwenty({ balance: 10.00 });
// Nothingwithdraw devuelve Nothing si hay fondos insuficientes. Este fallo intencional detiene la ejecución posterior en la cadena de map, evitando operaciones inválidas como actualizar registros sin retiro exitoso.
Liberando el Valor
Siempre llega el momento de ejecutar efectos: enviar JSON, imprimir, etc. No basta con devolver valores; debemos correr funciones que interactúen con el mundo. Como un koan zen: 《Si un programa no tiene efecto observable, ¿realmente se ejecuta?》.
Nuestra aplicación maneja datos hasta su salida final, que puede mapearse sin extraer el valor del contenedor. Intentar extraerlo previamente rompe la lógica, pues el valor podría no existir. Similar al gato de Schrödinger, mantenemos ambos estados hasta la función final.
Existe una salida de emergencia. Usando el helper maybe:
// maybe :: b -> (a -> b) -> Maybe a -> b
const maybe = curry((v, f, m) => {
if (m.isNothing) {
return v;
}
return f(m.$value);
});
// getTwenty :: Account -> String
const getTwenty = compose(maybe('You\'re broke!', finishTransaction), withdraw(20));
getTwenty({ balance: 200.00 });
// 'Your balance is $180.00'
getTwenty({ balance: 10.00 });
// 'You\'re broke!'Esto equivale a un if/else: maybe usa dos funciones (similar a Either), mientras map implementa if (x !== null).
Maybe puede resultar incómodo inicialmente, pero su seguridad es invaluable. Previene errores y asegura integridad, como materiales de construcción resistentes (nota: alusión al cuento de Los tres cerditos).
Software inseguro es como lanzar huevos pintados al tráfico. La seguridad en funciones con Maybe es crucial.
Implementaciones reales separan Maybe en dos tipos (Just/Nothing) para respetar parametricidad en map, permitiendo valores como null y manteniendo cualificaciones universales.
Manejo Puro de Errores

throw/catch no es puro. Either ofrece alternativa cortés para manejar errores:
Ver implementación en Apéndice B
class Either {
static of(x) {
return new Right(x);
}
constructor(x) {
this.$value = x;
}
}
class Left extends Either {
map(f) {
return this;
}
inspect() {
return `Left(${inspect(this.$value)})`;
}
}
class Right extends Either {
map(f) {
return Either.of(f(this.$value));
}
inspect() {
return `Right(${inspect(this.$value)})`;
}
}
const left = x => new Left(x);Left y Right son subtipos de Either. Veamos su comportamiento:
Either.of('rain').map(str => `b${str}`);
// Right('brain')
left('rain').map(str => `It's gonna ${str}, better bring your umbrella!`);
// Left('rain')
Either.of({ host: 'localhost', port: 80 }).map(prop('host'));
// Right('localhost')
left('rolls eyes...').map(prop('host'));
// Left('rolls eyes...')Left ignora las operaciones map, mientras Right actúa como Container. La magia está en incrustar mensajes de error en Left.
Calculemos edad desde fecha de nacimiento con Either:
const moment = require('moment');
// getAge :: Date -> User -> Either(String, Number)
const getAge = curry((now, user) => {
const birthDate = moment(user.birthDate, 'YYYY-MM-DD');
return birthDate.isValid()
? Either.of(now.diff(birthDate, 'years'))
: left('Birth date could not be parsed');
});
getAge(moment(), { birthDate: '2005-12-12' });
// Right(9)
getAge(moment(), { birthDate: 'July 4, 2001' });
// Left('Birth date could not be parsed')Similar a Maybe, pero con mensajes de error. Notar que devolvemos Either(String, Number), indicando posible error o valor numérico.
// fortune :: Number -> String
const fortune = compose(concat('If you survive, you will be '), toString, add(1));
// zoltar :: User -> Either(String, _)
const zoltar = compose(map(console.log), map(fortune), getAge(moment()));
zoltar({ birthDate: '2005-12-12' });
// 'If you survive, you will be 10'
// Right(undefined)
zoltar({ birthDate: 'balloons!' });
// Left('Birth date could not be parsed')Manejamos bifurcaciones lógicas de forma lineal, como alternativas condicionales sin bloches if/else. La función fortune permanece ajena a los functores, ejemplificando lifting (elevación funcional).
_ en firmas de tipo indica valor ignorado (en algunos navegadores requiere console.log.bind(console)).
Mantener funciones independientes de contenedores permite reutilización y lifting según necesidad, mejorando modularidad.
Either maneja errores desde validaciones hasta fallos críticos. Sustituir Maybe por Either mejora retroalimentación.
Either representa disyunción lógica (||) y coproductos (coproduct) en teoría de categorías. Como functor, se usa principalmente para manejo de errores.
El helper either toma dos funciones (como alternativa de if/else):
// either :: (a -> c) -> (b -> c) -> Either a b -> c
const either = curry((f, g, e) => {
let result;
switch (e.constructor) {
case Left:
result = f(e.$value);
break;
case Right:
result = g(e.$value);
break;
// No Default
}
return result;
});
// zoltar :: User -> _
const zoltar = compose(console.log, either(id, fortune), getAge(moment()));
zoltar({ birthDate: '2005-12-12' });
// 'If you survive, you will be 10'
// undefined
zoltar({ birthDate: 'balloons!' });
// 'Birth date could not be parsed'
// undefinedUsando id para propagar mensajes de error en Left, fortalecemos aplicaciones con manejo explícito. Así mantenemos flujos lineales pese a bifurcaciones.
El Viejo MacDonald Tenía Efectos...

En capítulos previos vimos funciones puras con efectos secundarios encapsulados. Ejemplo:
// getFromStorage :: String -> (_ -> String)
const getFromStorage = key => () => localStorage[key];Envolviendo efectos en funciones, garantizamos salidas consistentes. Como una figura de acción coleccionable en su empaque original, no podemos interactuar directamente sin desenvolver. Aquí entra IO.
IO difiere en que su $value siempre es función. Representa acciones impuras retrasadas. Al mapear, construimos flujos de operaciones sin ejecutarlas, como decisiones en cadena.
class IO {
static of(x) {
return new IO(() => x);
}
constructor(fn) {
this.$value = fn;
}
map(fn) {
return new IO(compose(fn, this.$value));
}
inspect() {
return `IO(${inspect(this.$value)})`;
}
}IO difiere de los functores anteriores en que el $value siempre es una función. No pensamos en su $value como función – eso es detalle de implementación que ignoraremos. Lo importante es que IO retrasa la acción impura encapsulándola, actuando como ambiente protegido para nuestros datos hasta la ejecución final.
Veámoslo en acción:
// ioWindow :: IO Window
const ioWindow = new IO(() => window);
ioWindow.map(win => win.innerWidth);
// IO(1430)
ioWindow
.map(prop('location'))
.map(prop('href'))
.map(split('/'));
// IO(['http:', '', 'localhost:8000', 'blog', 'posts'])
// $ :: String -> IO [DOM]
const $ = selector => new IO(() => document.querySelectorAll(selector));
$('#myDiv').map(head).map(div => div.innerHTML);
// IO('I am some inner html')ioWindow es IO mapeable. Cada map añade funciones al flujo sin ejecutarlas, similar al patrón Command. Las leyes de functores permiten operaciones intuitivas.
Dominados los functores, manipulamos valores impuros sin perder pureza. Pero eventualmente debemos ejecutar estos efectos:
El código puro delega la ejecución al llamador. Flujos profundos como IO(Maybe([x])) son válidos, permitiendo expresividad.
// url :: IO String
const url = new IO(() => window.location.href);
// toPairs :: String -> [[String]]
const toPairs = compose(map(split('=')), split('&'));
// params :: String -> [[String]]
const params = compose(toPairs, last, split('?'));
// findParam :: String -> IO Maybe [String]
const findParam = key => map(compose(Maybe.of, find(compose(eq(key), head)), params), url);
// -- Impure calling code ----------------------------------------------
// run it by calling $value()!
findParam('searchTerm').$value();
// Just(['searchTerm', 'wafflehouse'])El código mantiene limpieza delegando acciones impuras mediante composición.
Renombrando $value a unsafePerformIO enfatiza su naturaleza volátil, clarificando al usuario su responsabilidad de ejecución.
class IO {
constructor(io) {
this.unsafePerformIO = io;
}
map(fn) {
return new IO(compose(fn, this.unsafePerformIO));
}
}Ahora, findParam('searchTerm').unsafePerformIO() es claro. IO maneja efectos impuros. Próximamente: otro tipo similar.
Tareas Asíncronas
Los callbacks son un camino al infierno de anidación. Task (antes Future) de Folktale ofrece alternativa funcional:
La implementación detallada está fuera de alcance. Ejemplo básico usando Data.Task:
map sobre Task manipula valores futuros. Similar a Promesas, pero puras.
// -- Node readFile example ------------------------------------------
const fs = require('fs');
// readFile :: String -> Task Error String
const readFile = filename => new Task((reject, result) => {
fs.readFile(filename, (err, data) => (err ? reject(err) : result(data)));
});
readFile('metamorphosis').map(split('\n')).map(head);
// Task('One morning, as Gregor Samsa was waking up from anxious dreams, he discovered that
// in bed he had been changed into a monstrous verminous bug.')
// -- jQuery getJSON example -----------------------------------------
// getJSON :: String -> {} -> Task Error JSON
const getJSON = curry((url, params) => new Task((reject, result) => {
$.getJSON(url, params, result).fail(reject);
}));
getJSON('/video', { id: 10 }).map(prop('title'));
// Task('Family Matters ep 15')
// -- Default Minimal Context ----------------------------------------
// We can put normal, non futuristic values inside as well
Task.of(3).map(three => three + 1);
// Task(4)Las promesas no son puras, pero la analogía de map con then es útil. Task maneja asincronía pura.
Como IO, Task espera ejecución explícita via fork. Este modelo permite composiciones complejas sin bloques de callback.
Ejecutando con fork:
fork maneja asincronía sin bloquear. Mostramos spinner, procesamos respuesta/error al finalizar.
// -- Pure application -------------------------------------------------
// blogPage :: Posts -> HTML
const blogPage = Handlebars.compile(blogTemplate);
// renderPage :: Posts -> HTML
const renderPage = compose(blogPage, sortBy(prop('date')));
// blog :: Params -> Task Error HTML
const blog = compose(map(renderPage), getJSON('/posts'));
// -- Impure calling code ----------------------------------------------
blog({}).fork(
error => $('#error').html(error.message),
page => $('#main').html(page),
);
$('#spinner').show();Los flujos se leen linealmente, facilitando razonamiento frente a callbacks anidados.
Task además incorpora Either para manejo de fallos asíncronos. Genera código robusto.
Ejemplo complejo combinado usando validación síncrona con efectos:
Usando Either e IO dentro de Task, manejamos validaciones sincrónicas y efectos asíncronos limpiamente.
// Postgres.connect :: Url -> IO DbConnection
// runQuery :: DbConnection -> ResultSet
// readFile :: String -> Task Error String
// -- Pure application -------------------------------------------------
// dbUrl :: Config -> Either Error Url
const dbUrl = ({ uname, pass, host, db }) => {
if (uname && pass && host && db) {
return Either.of(`db:pg://${uname}:${pass}@${host}5432/${db}`);
}
return left(Error('Invalid config!'));
};
// connectDb :: Config -> Either Error (IO DbConnection)
const connectDb = compose(map(Postgres.connect), dbUrl);
// getConfig :: Filename -> Task Error (Either Error (IO DbConnection))
const getConfig = compose(map(compose(connectDb, JSON.parse)), readFile);
// -- Impure calling code ----------------------------------------------
getConfig('db.json').fork(
logErr('couldn\'t read file'),
either(console.log, map(runQuery)),
);Aunque faltan técnicas avanzadas (mónadas), con map logramos mucho. Ahora: bases matemáticas.
Un Toque de Teoría
Los functores vienen de teoría de categorías y siguen leyes:
La ley de identidad es fundamental. Código verificable garantiza cumplimiento:
Las composiciones también cumplen:
// identity
map(id) === id;
// composition
compose(map(f), map(g)) === map(compose(f, g));En teoría, functores mapean objetos y morfismos entre categorías, preservando identidades y composiciones.
const idLaw1 = map(id);
const idLaw2 = id;
idLaw1(Container.of(2)); // Container(2)
idLaw2(Container.of(2)); // Container(2)Piense categorías como redes de objetos conectados por morfismos. El functor F mapea categoría C a D, preservando estructura.
const compLaw1 = compose(map(append(' world')), map(append(' cruel')));
const compLaw2 = map(compose(append(' world'), append(' cruel')));
compLaw1(Container.of('Goodbye')); // Container('Goodbye cruel world')
compLaw2(Container.of('Goodbye')); // Container('Goodbye cruel world')Por ejemplo, Maybe mapea tipos a categoría con posible nulidad. En código, esto se implementa con map y verificaciones.
Diagrama explicando conservación de propiedades:

La conmutatividad de diagramas permite razonamiento funcional robusto.
Ejemplo visual sobre composiciones:

Propiedades de functores permiten refactorizaciones seguras.
// topRoute :: String -> Maybe String
const topRoute = compose(Maybe.of, reverse);
// bottomRoute :: String -> Maybe String
const bottomRoute = compose(map(reverse), Maybe.of);
topRoute('hi'); // Just('ih')
bottomRoute('hi'); // Just('ih')Los functores pueden anidarse:

Contenedores anidados como IO(Maybe([x])) requieren múltiples map, pero la composición funcional simplifica esto.
La composición asociativa de functores forma categorías complejas. Identity es functor base.
const nested = Task.of([Either.of('pillows'), left('no sleep for you')]);
map(map(map(toUpperCase)), nested);
// Task([Right('PILLOWS'), Left('no sleep for you')])En Resumen
class Compose {
constructor(fgx) {
this.getCompose = fgx;
}
static of(fgx) {
return new Compose(fgx);
}
map(fn) {
return new Compose(map(map(fn), this.getCompose));
}
}
const tmd = Task.of(Maybe.of('Rock over London'));
const ctmd = Compose.of(tmd);
const ctmd2 = map(append(', rock on, Chicago'), ctmd);
// Compose(Task(Just('Rock over London, rock on, Chicago')))
ctmd2.getCompose;
// Task(Just('Rock over London, rock on, Chicago'))Functores son ubicuos: listas, árboles, streams, etc. Su uso extensivo facilita programación funcional robusta.
Faltan técnicas para múltiples argumentos y secuencias complejas, que abordaremos con mónadas.
Capítulo 09: Mónadas y Cebollas
Ejercicios
¡Practiquemos!
Usa add y map para crear función que incremente un valor dentro de un functor.
Dado el siguiente objeto User:
// incrF :: Functor f => f Int -> f Int
const incrF = undefined;¡Practiquemos!
Usa safeProp y head para encontrar la inicial del usuario.
const user = { id: 2, name: 'Albert', active: true };
Dadas las siguientes funciones helper:
// initial :: User -> Maybe String
const initial = undefined;¡Practiquemos!
Escribe función que use checkActive y showWelcome para dar acceso o devolver error.
// showWelcome :: User -> String const showWelcome = compose(concat('Welcome '), prop('name')); // checkActive :: User -> Either String User const checkActive = function checkActive(user) { return user.active ? Either.of(user) : left('Your account is not active'); };
Ahora consideremos:
// eitherWelcome :: User -> Either String String
const eitherWelcome = undefined;¡Practiquemos!
Escribe validateName que verifique si nombre de usuario tiene más de 3 caracteres o devuelva error. Luego usa either, showWelcome y save para hacer función register.
Ambos argumentos de either deben devolver mismo tipo.
// validateUser :: (User -> Either String ()) -> User -> Either String User
const validateUser = curry((validate, user) => validate(user).map(_ => user));
// save :: User -> IO User
const save = user => new IO(() => ({ ...user, saved: true }));
$$114$$
// validateName :: User -> Either String ()
const validateName = undefined;
// register :: User -> IO String
const register = compose(undefined, validateUser(validateName));$$115$$