Skip to content

Capítulo 11: Transformar Nuevamente con Naturales

Vamos a discutir las transformaciones naturales en el contexto de su utilidad práctica en código cotidiano. Estas constituyen un pilar de la teoría de categorías y son absolutamente indispensables al aplicar matemáticas para razonar y refactorizar código. Por tanto, considero mi deber advertirles sobre la lamentable injusticia que presenciarán sin duda debido a mi alcance limitado. Comencemos.

Maldición a este anidamiento

Quiero abordar el problema del anidamiento. No me refiero al impulso instintivo de futuros padres que ordenan obsesivamente, aunque... reflexionándolo bien, la analogía no está tan lejos de la realidad como veremos en próximos capítulos... Hablo específicamente de tener dos o más tipos diferentes agrupados alrededor de un valor, protegiéndolo como a un recién nacido.

js
Right(Maybe('b'));

IO(Task(IO(1000)));

[Identity('bee thousand')];

Hasta ahora evitamos este escenario con ejemplos cuidadosos, pero en la práctica, al programar, los tipos suelen enredarse como audífonos poseídos. Si no organizamos meticulosamente los tipos, nuestro código parecerá más enmarañado que un beatnik en una cafetería de gatos.

Comedia situacional

js
// getValue :: Selector -> Task Error (Maybe String)
// postComment :: String -> Task Error Comment
// validate :: String -> Either ValidationError String

// saveComment :: () -> Task Error (Maybe (Either ValidationError (Task Error Comment)))
const saveComment = compose(
  map(map(map(postComment))),
  map(map(validate)),
  getValue('#comment'),
);

Presentemos el código problemático. Empezamos con getValue('#comment') que retorna Task Error (Maybe String) (Tarea con Error y posible String). Luego mapeamos sobre Task y Maybe para pasar el texto a validate, obteniendo Either ValidationError String. Finalmente, mapeamos repetidamente para enviar el String a postComment, resultando en Task Error (Maybe (Either ValidationError String)).

Un verdadero caos: collage de tipos abstractos, Pollock polimórfico, Mondrian monolítico. Las soluciones incluyen componer contenedores monstruosos, homologar efectos, etc. En este capítulo nos enfocaremos en homogeneizar mediante transformaciones naturales.

Totalmente natural

Una Transformación Natural es un "morfismo entre funtores": función que opera en contenedores preservando estructura. Tipográficamente es (Functor f, Functor g) => f a -> g a. La clave es no inspeccionar el contenido, funcionando como intercambio de información clasificada. Formalmente debe cumplir:

natural transformation diagram

En código:

js
// nt :: (Functor f, Functor g) => f a -> g a
compose(map(f), nt) === compose(nt, map(f));

Tanto el diagrama como el código establecen que aplicar la transformación antes/después de mapear produce el mismo resultado. Esto deriva de teoremas libres, aunque las transformaciones naturales no se limitan a funciones sobre tipos.

Conversiones tipológicas fundamentadas

Como programadores, conocemos conversiones básicas. La diferencia aquí radica en trabajar con contenedores algebraicos y teoría categórica.

Ejemplos ilustrativos:

js
// idToMaybe :: Identity a -> Maybe a
const idToMaybe = x => Maybe.of(x.$value);

// idToIO :: Identity a -> IO a
const idToIO = x => IO.of(x.$value);

// eitherToTask :: Either a b -> Task a b
const eitherToTask = either(Task.rejected, Task.of);

// ioToTask :: IO a -> Task () a
const ioToTask = x => new Task((reject, resolve) => resolve(x.unsafePerform()));

// maybeToTask :: Maybe a -> Task () a
const maybeToTask = x => (x.isNothing ? Task.rejected() : Task.of(x.$value));

// arrayToMaybe :: [a] -> Maybe a
const arrayToMaybe = x => Maybe.of(x[0]);

¿Captan la idea? Transformamos funtores preservando valores. Podemos perder información secundaria, pero el valor mapeado debe mantenerse. El punto crucial es que map sigue funcionando tras la transformación.

Alternativamente, transformamos efectos: ioToTask convierte sincronía a asincronía, arrayToMaybe de no determinismo a falla potencial. Notar que en JS no podemos escribir taskToIO (sería transformación supernatural).

Envidia funcional

Al necesitar características de otro tipo (ej: sortBy en List), las transformaciones naturales permiten conversión segura preservando la capacidad de mapeo.

js
// arrayToList :: [a] -> List a
const arrayToList = List.of;

const doListyThings = compose(sortBy(h), filter(g), arrayToList, map(f));
const doListyThings_ = compose(sortBy(h), filter(g), map(f), arrayToList); // law applied

Con un toque mágico (arrayToList), convertimos [a] en List a para usar sortBy libremente.

Además, optimizamos operaciones moviendo map(f) a la izquierda de la transformación natural (ver doListyThings_).

JavaScript isomórfico

Cuando podemos convertir bidireccionalmente sin perder datos, tenemos un isomorfismo. Dos tipos son isomórficos si existen transformaciones naturales inversas:

js
// promiseToTask :: Promise a b -> Task a b
const promiseToTask = x => new Task((reject, resolve) => x.then(resolve).catch(reject));

// taskToPromise :: Task a b -> Promise a b
const taskToPromise = x => new Promise((resolve, reject) => x.fork(reject, resolve));

const x = Promise.resolve('ring');
taskToPromise(promiseToTask(x)) === x;

const y = Task.of('rabbit');
promiseToTask(taskToPromise(y)) === y;

Q.E.D. Promise y Task son isomórficos. arrayToMaybe no lo es pues pierde información, aunque sigue siendo transformación natural al preservar map.

js
// maybeToArray :: Maybe a -> [a]
const maybeToArray = x => (x.isNothing ? [] : [x.$value]);

// arrayToMaybe :: [a] -> Maybe a
const arrayToMaybe = x => Maybe.of(x[0]);

const x = ['elvis costello', 'the attractions'];

// not isomorphic
maybeToArray(arrayToMaybe(x)); // ['elvis costello']

// but is a natural transformation
compose(arrayToMaybe, map(replace('elvis', 'lou')))(x); // Just('lou costello')
// ==
compose(map(replace('elvis', 'lou')), arrayToMaybe)(x); // Just('lou costello')

Los isomorfismos son conceptos poderosos y ubicuos, pero continuemos.

Definición ampliada

Estas funciones estructurales superan las conversiones básicas:

Ejemplos diversos:

hs
reverse :: [a] -> [a]

join :: (Monad m) => m (m a) -> m a

head :: [a] -> a

of :: a -> f a

Las leyes naturales se aplican aquí también. Observar que head :: [a] -> a puede verse como head :: [a] -> Identity a, aprovechando que a e Identity a son isomórficos.

Solución al anidamiento

Volviendo a nuestra firma de tipos problemática: usando transformaciones naturales en puntos estratégicos, forzamos uniformidad tipológica permitiendo join.

js
// getValue :: Selector -> Task Error (Maybe String)
// postComment :: String -> Task Error Comment
// validate :: String -> Either ValidationError String

// saveComment :: () -> Task Error Comment
const saveComment = compose(
  chain(postComment),
  chain(eitherToTask),
  map(validate),
  chain(maybeToTask),
  getValue('#comment'),
);

Al añadir chain(maybeToTask) y chain(eitherToTask), convertimos funtores anidados a Task y los unimos. Como dice el refrán: "Más vale prevenir que lamentar".

Resumiendo

Las transformaciones naturales operan sobre funtores, siendo cruciales en teoría de categorías. Al homogeneizar tipos, permiten composición garantizada aunque simplifiquen a efectos más volátiles (usualmente Task).

Esta gestión tipológica es el costo de materializar efectos. Necesitaremos más herramientas (como Traversable) para abordar amalgamas complejas.

Capítulo 12: Atravesando la piedra

Ejercicios

¡Practiquemos!

Escribe una transformación natural que convierta Either b a a Maybe a // eitherToMaybe :: Either b a -> Maybe a const eitherToMaybe = undefined;


js
// eitherToTask :: Either a b -> Task a b
const eitherToTask = either(Task.rejected, Task.of);

¡Practiquemos!

Usando eitherToTask, simplifica findNameById para eliminar el Either anidado. // findNameById :: Number -> Task Error (Either Error User) const findNameById = compose(map(map(prop('name'))), findUserById);


Recordatorio: las siguientes funciones están disponibles en el contexto:

hs
split :: String -> String -> [String]
intercalate :: String -> [String] -> String

¡Practiquemos!

Escribe los isomorfismos entre String y [Char]. // strToList :: String -> [Char] const strToList = undefined; // listToStr :: [Char] -> String const listToStr = undefined;