Capítulo 12: Manipulación de Estructuras
En nuestro circo de contenedores hasta ahora, nos has visto domar al feroz functor, adaptándolo para realizar cualquier operación que deseemos. Te ha maravillado cómo manejamos múltiples efectos mediante aplicación funcional para obtener resultados. Contemplaste cómo los contenedores se fusionan al concatenarlos mediante join. En el espectáculo de efectos secundarios, los vimos componerse en uno. Finalmente, presenciaste la transformación de tipos ante tus ojos.
En nuestro próximo acto, exploraremos las travesías. Veremos tipos interactuar como volatineros manteniendo valores intactos. Reordenaremos efectos como vagones en carruseles giratorios. Cuando los contenedores se enreden como miembros de acróbatas, esta interfaz los alineará. Observaremos efectos según su ordenamiento. Pasen mis zapatones de payaso y chiflo mágico, ¡comencemos!
Tipos y más Tipos
Adentrémonos en lo peculiar:
// readFile :: FileName -> Task Error String
// firstWords :: String -> String
const firstWords = compose(intercalate(' '), take(3), split(' '));
// tldr :: FileName -> Task Error String
const tldr = compose(map(firstWords), readFile);
map(tldr, ['file1', 'file2']);
// [Task('hail the monarchy'), Task('smash the patriarchy')]Al leer varios archivos obtenemos un array inútil de tareas. ¿Cómo procesarlas? Necesitamos transformar [Task Error String] a Task Error [String]. Así tendríamos un futuro con todos los resultados, más práctico para operaciones asíncronas.
Situación compleja final:
// getAttribute :: String -> Node -> Maybe String
// $ :: Selector -> IO Node
// getControlNode :: Selector -> IO (Maybe (IO Node))
const getControlNode = compose(map(map($)), map(getAttribute('aria-controls')), $);Estos IO anhelan unirse. Quisiéramos aplicar join para fusionarlos, pero el Maybe actúa como barrera. La solución es reordenarlos para obtener IO (Maybe Node).
Armonía Tipológica
La interfaz Traversable contiene: sequence y traverse.
Reorganicemos con sequence:
sequence(List.of, Maybe.of(['the facts'])); // [Just('the facts')]
sequence(Task.of, new Map({ a: Task.of(1), b: Task.of(2) })); // Task(Map({ a: 1, b: 2 }))
sequence(IO.of, Either.of(IO.of('buckle my shoe'))); // IO(Right('buckle my shoe'))
sequence(Either.of, [Either.of('wing')]); // Right(['wing'])
sequence(Task.of, left('wing')); // Task(Left('wing'))¿Notan el cambio? El tipo anidado se invirtió completamente. sequence exige que su argumento sea Traversable conteniendo Applicative.
// sequence :: (Traversable t, Applicative f) => (a -> f a) -> t (f a) -> f (t a)
const sequence = curry((of, x) => x.sequence(of));Transforma t (f a) en f (t a). El primer argumento (constructor) ayuda con tipos complejos como Left en lenguajes no tipados.
Veamos la implementación en Either:
class Right extends Either {
// ...
sequence(of) {
return this.$value.map(Either.of);
}
}Al ser el valor un applicative, mapeamos el constructor para modificar tipos.
Ignoramos of excepto en casos como Left donde se necesita preservar estructura. Applicative requiere un Pointed Functor.
class Left extends Either {
// ...
sequence(of) {
return of(this);
}
}En lenguajes tipados, el tipo exterior se infiere automáticamente.
Gama de Efectos
Distinto orden = distintos comportamientos: [Maybe a] versus Maybe [a]. Either Error (Task Error a) es validación cliente; Task Error (Either Error a) validación servidor.
// fromPredicate :: (a -> Bool) -> a -> Either e a
// partition :: (a -> Bool) -> [a] -> [Either e a]
const partition = f => map(fromPredicate(f));
// validate :: (a -> Bool) -> [a] -> Either e [a]
const validate = f => traverse(Either.of, fromPredicate(f));partition agrupa éxitos/fracasos; validate captura primer error. El orden tipológico define el comportamiento.
Analicemos List.traverse para construir validate:
traverse(of, fn) {
return this.$value.reduce(
(f, a) => fn(a).map(b => bs => bs.concat(b)).ap(f),
of(new List([])),
);
}Usando reduce con función acumuladora. Desglosemos:
reduce(..., ...)
Firma: reduce :: [a] -> (f -> a -> f) -> f -> f. El primer argumento viene de $value.
of(new List([]))
Semilla inicial: Either e [a] igual al tipo final.
fn :: Applicative f => a -> f a
En nuestro caso: fromPredicate(f) :: a -> Either e a > fn(a)::Either e a
.map(b => bs => bs.concat(b))
En Right mapea para producir función. Tipo: Either e (\\[a] -> \\[a])
.ap(f)
Aplica función acumuladora a f. Mantiene tipo Either e \\[a] tanto en éxito como error.
Esta transformación de 6 líneas usando of, map y ap funciona para cualquier Applicative, demostrando el poder de la abstracción.
Danza Tipológica
Revisemos los ejemplos iniciales:
// readFile :: FileName -> Task Error String
// firstWords :: String -> String
const firstWords = compose(intercalate(' '), take(3), split(' '));
// tldr :: FileName -> Task Error String
const tldr = compose(map(firstWords), readFile);
traverse(Task.of, tldr, ['file1', 'file2']);
// Task(['hail the monarchy', 'smash the patriarchy']);Usando traverse coordinamos Tasks como Promise.all(), solución genérica para tipos traversables.
Refinemos el último ejemplo:
// getAttribute :: String -> Node -> Maybe String
// $ :: Selector -> IO Node
// getControlNode :: Selector -> IO (Maybe Node)
const getControlNode = compose(chain(traverse(IO.of, $)), map(getAttribute('aria-controls')), $);Reemplazamos map(map($)) con chain(traverse(IO.of, $)) invirtiendo tipos mediante mapeo y aplanamiento.
Sin Leyes ni Órden
Estas leyes brindan garantías críticas. La arquitectura busca restricciones que conduzcan a soluciones elegantes.
Interfaces sin leyes son indirecciones vacías. Como estructuras matemáticas, deben exponer propiedades verificables.
¡Exploremos estas leyes!
Identidad
const identity1 = compose(sequence(Identity.of), map(Identity.of));
const identity2 = Identity.of;
// test it out with Right
identity1(Either.of('stuff'));
// Identity(Right('stuff'))
identity2(Either.of('stuff'));
// Identity(Right('stuff'))Al aplicar sequence a Identity preservamos estructura. Usando Right para verificación: en teoría de categorías, transformaciones naturales son morfismos. Identity es fundamental como compose.
Composición
const comp1 = compose(sequence(Compose.of), map(Compose.of));
const comp2 = (Fof, Gof) => compose(Compose.of, map(sequence(Gof)), sequence(Fof));
// Test it out with some types we have lying around
comp1(Identity(Right([true])));
// Compose(Right([Identity(true)]))
comp2(Either.of, Array)(Identity(Right([true])));
// Compose(Right([Identity(true)]))La composición preserva estructura. Herramientas como quickcheck verifican mediante pruebas aleatorias.
Esto permite fusión de travesías, optimizando rendimiento.
Naturalidad
const natLaw1 = (of, nt) => compose(nt, sequence(of));
const natLaw2 = (of, nt) => compose(sequence(of), map(nt));
// test with a random natural transformation and our friendly Identity/Right functors.
// maybeToEither :: Maybe a -> Either () a
const maybeToEither = x => (x.$value ? new Right(x.$value) : new Left());
natLaw1(Maybe.of, maybeToEither)(Identity.of(Maybe.of('barlow one')));
// Right(Identity('barlow one'))
natLaw2(Either.of, maybeToEither)(Identity.of(Maybe.of('barlow one')));
// Right(Identity('barlow one'))Aplicar transformación natural tras invertir tipos equivale a mapear primero y luego invertir.
Esto permite optimizaciones:
traverse(A.of, A.of) === A.of;Ventaja crucial para rendimiento.
Conclusión
Traversable permite reorganizar tipos con precisión quirúrgica. Diferentes órdenes generan distintos efectos. Próximo acto: Monoides: Unificando Todo
Ejercicios
Considera:
// httpGet :: Route -> Task Error JSON
// routes :: Map Route Route
const routes = new Map({ '/': '/', '/about': '/about' });¡Practiquemos!
Usa traversable para transformar getJsons a:
Map Route Route → Task Error (Map Route JSON)
// getJsons :: Map Route Route -> Map Route (Task Error JSON)
const getJsons = map(httpGet);
Definimos función de validación:
// validate :: Player -> Either String Player
const validate = player => (player.name ? Either.of(player) : left('must have name'));¡Practiquemos!
Usa traversable y validate para que startGame solo inicie con jugadores válidos
// startGame :: [Player] -> [Either Error String]
const startGame = compose(map(map(always('game started!'))), map(validate));
Helpers de sistema de archivos:
// readfile :: String -> String -> Task Error String
// readdir :: String -> Task Error [String]¡Practiquemos!
Reordena y aplana Task & Maybe anidados usando traversable // readFirst :: String -> Task Error (Maybe (Task Error String)) const readFirst = compose(map(map(readfile('utf-8'))), map(safeHead), readdir);