ÂŋQuÃĐ pasarÃa si pudiÃĐramos extraer la idea detrÃĄs de operaciones como map y filter y aplicarlas a otro tipo de colecciones mÃĄs allÃĄ de los arreglos? ÂŋY si les digo que puedo implementar filter una sola vez y reusar ese mismo cÃģdigo en diferentes tipos de colecciones? Esa es la premisa de los transductores. Hoy vamos a aprender quÃĐ son, cÃģmo funcionan y cÃģmo se usan.
Requerimientos
Antes de comenzar hay un par de cosas que necesitan saber:
TambiÃĐn es recomendado que tengan familiaridad con los siguientes conceptos:
- Funciones de primera clase
- Funciones de orden superior
- Cierres (closures)
Y si no estÃĄn al tanto de quÃĐ significa todo eso, no se preocupen. SÃģlo deben saber que en javascript podemos tratar a las funciones como cualquier otro tipo de dato.
Comencemos.
ÂŋQuÃĐ son los transductores?
La palabra transductor tiene una larga historia. Si buscan su definiciÃģn se van a encontrar con algo como esto:
Un transductor es un dispositivo capaz de transformar o convertir una determinada manifestaciÃģn de energÃa de entrada, en otra diferente de salida...
-- Wikipedia
Definitivamente no estamos hablando de dispositivos fÃsicos en este artÃculo. Pero sà se acerca a lo que queremos, el objetivo principal de un transductor (en nuestro contexto) serÃĄ procesar los datos de una colecciÃģn y potencialmente convertir esa colecciÃģn de un tipo de dato a otro.
Para nuestros propÃģsitos una definiciÃģn mÃĄs cercana a lo que queremos serÃa esta:
Transformaciones algorÃtmicas combinables.
Ya sÃĐ, no parece que esa tampoco ayude mucho. Bueno, la idea aquà es bÃĄsicamente combinar procesos de una manera declarativa, y tambiÃĐn que sea reusable en diferentes estructuras. Eso es todo. Pero claro es mÃĄs fÃĄcil decirlo que hacerlo.
ÂŋCÃģmo logramos todo eso?
Buena pregunta. Esto serÃĄ todo un viaje, mejor empecemos con pasos pequeÃąos. Primero preguntemos...
ÂŋPor quÃĐ?
Usemos un ejemplo para responder eso. Imaginemos un escenario comÚn. Digamos que tenemos un arreglo y queremos filtrarlo. ÂŋCÃģmo lo hacemos? Usamos el mÃĐtodo .filter.
const is_even = number => number % 2 === 0;
const data = [1, 2, 3];
data.filter(is_even);
// Array [ 2 ]
Todo se ve bien. Ahora nos llega otro requerimiento, tenemos que transformar los valores que pasan la prueba de la funciÃģn is_even. No hay problema porque podemos usar .map.
const is_even = number => number % 2 === 0;
const add_message = number => `The number is: ${number}`;
const data = [1, 2, 3];
data.filter(is_even).map(add_message);
// Array [ "The number is: 2" ]
Genial. Todo funciona bien hasta que un dÃa, por razones que no vamos discutir, nos vemos obligados a convertir data en un Set. DespuÃĐs de hacer el cambio nos topamos con este mensaje.
Uncaught TypeError: data.filter is not a function
ÂŋCÃģmo podemos resolver esto? Una forma serÃa usar el ciclo for..of.
const is_even = number => number % 2 === 0;
const add_message = number => `The number is: ${number}`;
const data = new Set([1, 2, 3]);
const filtered = new Set();
for(let number of data) {
if(is_even(number)) {
filtered.add(add_message(number));
}
}
filtered;
// Set [ "The number is: 2" ]
La buena noticia es que esto funciona con cualquier estructura que implemente el protocolo de iteraciÃģn. La mala noticia es que para agregar otra "operaciÃģn" tenemos que modificar el cÃģdigo de nuestro for.
ÂŋPor quÃĐ modificar el cÃģdigo serÃa un problema?
Hagamos una comparaciÃģn. Digamos que tenemos nuestro ciclo en su lugar.
for(let number of data) {
}
ÂŋQuÃĐ hacemos cuando queremos filtrar? Agregamos cÃģdigo dentro del bloque.
for(let number of data) {
+ if(is_even(number)) {
+ filtered.add(number);
+ }
}
ÂŋQuÃĐ hacemos cuando queremos transformar? Agregamos cÃģdigo dentro del bloque.
for(let number of data) {
if(is_even(number)) {
- filtered.add(number);
+ filtered.add(add_message(number));
}
}
Eso va ocurrir cada vez que queramos agregar alguna funcionalidad a nuestro ciclo. ÂŋAlguna vez han escuchado la frase "abierto para extensiÃģn, cerrado para modificaciÃģn"? Es bÃĄsicamente lo que quiero ilustrar aquÃ. Para extender el ciclo for necesitamos modificarlo, no es que sea una terrible idea, es sÃģlo que hay una forma mÃĄs "elegante" de lograr nuestro objetivo.
Revisemos nuevamente nuestra primera versiÃģn, la que tenÃa data como un Array. ÂŋQuÃĐ hacemos cuando necesitamos filtrar? Agregamos una funciÃģn.
data.filter(is_even);
ÂŋQuÃĐ hacemos cuando queremos transformar? Agregamos una funciÃģn.
- data.filter(is_even);
+ data.filter(is_even).map(add_message);
ÂŋVen a donde quiero llegar? No voy a decir que es mejor, sÃģlo digamos que es mÃĄs "expresivo". En este caso, para extender nuestro proceso lo que hacemos es combinar funciones.
Pero no todo es color de rosas. Ya nos topamos con un problema: no todas las colecciones implementan estos mÃĐtodos. Y otro problema que podrÃamos enfrentar tiene que ver con el desempeÃąo, porque cada mÃĐtodo es el equivalente a un ciclo for. Asà que tal vez no sea una buena idea hacer una larga cadena de filters y maps.
Aquà es donde entran los transductores, con ellos podemos construir una cadena de operaciones de una manera declarativa y eficiente. Aunque no serÃĄn tan rÃĄpidos como un ciclo for, puede ser una manera de aumentar el desempeÃąo cuando tienen una larga cadena de operaciones actuando sobre una colecciÃģn con muchos (muchos) elementos.
Otra cosa en la que destacan sobre los mÃĐtodos tradicionales en el prototipo Array es que podemos reusar la misma operaciÃģn en distintas estructuras. Podemos por ejemplo implementar filter como un transductor una vez y reusamos ese mismo cÃģdigo para los arreglos, Sets, generadores u otro tipo de colecciones. Suena genial, Âŋcierto?
ÂŋCÃģmo funcionan?
La magia detrÃĄs de los transductores se encuentra dentro de un concepto que mencionÃĐ en la secciÃģn de requerimientos: reducer. Especificamente reducers de orden superior. (Se los advertÃ).
Tomen un momento para digerir esta frase: "reducer de orden superior."
ÂŋEstÃĄn listos?
Por ahora podemos pensar en un transductor como una funciÃģn que puede recibir un reducer como argumento y retorna otro reducer. Resulta que (con un poco de magia) podemos combinar reducers usando composiciÃģn de funciones. Esta propiedad es la que nos va a permitir armar cadenas de operaciones como en nuestro ejemplo anterior, donde llamamos al mÃĐtodo .filter y luego .map. Pero hay una gran diferencia, la manera en la que se combinan de hecho deberÃa ser algo asÃ.
compose(filter(is_even), map(add_message));
Antes de que pregunten, la magia no ocurre en compose. Esa funciÃģn es bastante genÃĐrica. Lo Único que hace es pasar el resultado de una funciÃģn a la otra. Podemos implementarla nosotros mismos.
function compose(...fns) {
const apply = (arg, fn) => fn(arg);
return (initial) => fns.reduceRight(apply, initial);
}
Ahora, cuando combinamos varios transductores con compose lo que obtenemos es otro transductor. Pero ese no es el fin de la historia, como ya mencionÃĐ un transductor nos devuelve un reducer, ÂŋY quÃĐ funciÃģn conocen ustedes que necesite un reducer? Por supuesto, nuestro amigo reduce. reduce serÃĄ como el "protocolo" que usaremos para recorrer los valores de la colecciÃģn y hacer algo con ellos.
Creo que ya es suficiente de teorÃas, vamos a la prÃĄctica. Para empezar vamos a crear un transductor con la misma funcionalidad de filter.
Creando un transductor
Paso 1: Reunir los argumentos necesarios
Primero lo primero, creamos una funciÃģn y obtenemos todo lo que necesitamos con los parÃĄmetros. ÂŋQuÃĐ necesitamos en este caso? Una funciÃģn que deberÃa retornar true o false, un predicado.
function filter(predicate) {
}
Un buen comienzo pero no es suficiente. Sabemos que en algÚn momento vamos a combinar este transductor con otro. Lo que necesitamos ahora es aceptar otro reducer, que vendrÃa siendo el siguiente "paso" en la composiciÃģn. Vamos a agregar eso.
function filter(predicate, next) {
}
Si aÚn no estÃĄ claro, recordemos nuestro ejemplo anterior.
compose(filter(is_even), map(add_message));
Aquà lo que va a pasar es que map(add_message) nos devolverÃĄ un reducer. Ese reducer se convertirÃĄ en nuestro parÃĄmetro next.
Ya sÃĐ lo que piensan, sÃģlo estoy enviando el argumento is_even. ÂŋCÃģmo vamos a obtener next? Vamos a lidiar con eso despuÃĐs. Sigamos.
Paso 2: Retornar un reducer
En la prÃĄctica un reducer no es mÃĄs que una funciÃģn binaria. SÃģlo necesitamos retornar eso.
function filter(predicate, next) {
return function reducer(state, value) {
// ???
};
}
Paso 3: Implementa el resto
Bien, ya (casi) terminamos con la estructura del transductor. Lo que viene ahora es la lÃģgica que queremos implementar. En este caso, lo que queremos hacer es replicar el comportamiento de Array.filter.
function filter(predicate, next) {
return function reducer(state, value) {
if(predicate(value)) {
return next(state, value);
}
return state;
};
}
Aquà tomamos el predicado, lo evaluamos, y decidimos si vamos seguir con el siguiente paso o no hacemos nada.
Paso 4: AplicaciÃģn parcial
Aquà viene la magia. Sabemos cÃģmo queremos usar filter pero justo ahora no va a funcionar. Necesitamos que filter sea lo suficientemente inteligente para saber cuando tiene que ejecutarse, ÂŋCuÃĄndo es eso? Cuando tenga todos sus argumentos.
function filter(predicate, next) {
if(arguments.length === 1) {
return (_next) => filter(predicate, _next);
}
return function reducer(state, value) {
if(predicate(value)) {
return next(state, value);
}
return state;
};
}
Esta es sÃģlo una forma de lograr la aplicaciÃģn parcial. No tiene que ser de esta manera.
Usando un transductor
Ya tenemos algo que en teorÃa deberÃa funcionar. Ahora necesitamos una funciÃģn reduce. Por suerte para nosotros el prototipo Array tiene una que podemos usar. Empecemos usando un solo transductor.
const is_even = number => number % 2 === 0;
const data = [1, 2, 3];
const combine = (state, value) => (state.push(value), state);
data.reduce(filter(is_even, combine), []);
// Array [ 2 ]
ÂĄGenial, de verdad funciona! Ahora vamos a expandir el conjunto de datos. Digamos que ahora data tendrÃĄ nÚmeros negativos, pero tampoco queremos esos, vamos a crear otro filtro que deje pasar sÃģlo los nÚmeros positivos. Aquà es donde la composiciÃģn entra en escena.
const is_even = number => number % 2 === 0;
const is_positive = number => number > 0;
const data = [-2, -1, 0, 1, 2, 3];
const combine = (state, value) => (state.push(value), state);
const transducer = compose(filter(is_positive), filter(is_even));
data.reduce(transducer(combine), []);
// Array [ 2 ]
ÂŋVieron? Obtuvimos el mismo resultado. Ahora hagamos algo mejor, vamos a aÃąadir otra "operaciÃģn."
function map(transform, next) {
if(arguments.length === 1) {
return (_next) => map(transform, _next);
}
return function reducer(state, value) {
return next(state, transform(value));
};
}
El comportamiento es el mismo que esperarÃan de Array.map. Aquà el valor es transformado antes de ir al siguiente paso. Ahora vamos a incorporarlo en el ejemplo.
const data = [-2, -1, 0, 1, 2, 3];
const transducer = compose(
filter(is_positive),
filter(is_even),
map(add_message)
);
data.reduce(transducer(combine), []);
// Array [ "The number is: 2" ]
Esto es bueno, muy bueno. Hay un detalle que necesitamos atender, la compatibilidad. Les mencionÃĐ que los transductores deberÃan funcionar con otros tipos de colecciones aparte de Array, pero aquà usamos Array.reduce. El asunto es que para completar el panorama tenemos que controlar la funciÃģn reduce, asà que haremos una.
Ya que javascript nos ofrece el protocolo de iteraciÃģn, vamos a usar eso para ahorrarnos muchas molestias en nuestro propio reduce, con esto haremos que nuestros transductores sean compatibles con mÃĄs tipos de colecciones.
function reduce(reducer, initial, collection) {
let state = initial;
for(let value of collection) {
state = reducer(state, value);
}
return state;
}
Para probar esto cambiaremos nuestro ejemplo, data pasarÃĄ de ser un arreglo a un Set. Cambiaremos la funciÃģn combine, para que ahora estÃĐ al tanto de cÃģmo armar un Set. TambiÃĐn cambiaremos nuestro valor inicial en reduce a un Set. Lo demÃĄs seguirÃĄ igual.
const data = new Set([-2, -1, 0, 1, 2, 3]);
const combine = (state, value) => state.add(value);
const transducer = compose(
filter(is_positive),
filter(is_even),
map(add_message)
);
reduce(transducer(combine), new Set(), data);
// Set [ "The number is: 2" ]
Noten que el resultado no tiene porque ser un Set, podemos transformar data a un Array si eso deseamos. Para cambiar de un tipo de colecciÃģn a otro, sÃģlo tenemos que intercambiar el valor inicial en reduce y cambiar la funciÃģn combine.
Todo funciona bien pero hay una cosa mÃĄs que podemos hacer para crear una "experiencia" mÃĄs agradable. Hagamos una funciÃģn auxiliar, transduce, para que se encargue de algunos detalles por nosotros.
function transduce(combine, initial, transducer, collection) {
return reduce(transducer(combine), initial, collection);
}
No parece una gran mejora pero esto nos permite aumentar nuestro control sobre reduce, ahora podrÃamos tener varias implementaciones para diferentes estructuras y decidir cual queremos usar basados en el tipo de dato de collection. Pero por el momento sÃģlo usaremos la funciÃģn reduce que creamos anteriormente.
Ahora lo que haremos serÃĄ encargarnos de algunos detalles antes de tiempo. Crearemos funciones que tengan la misma funcionalidad de combine, para acumular los valores finales y asociamos eso con el valor inicial correcto.
function curry(arity, fn, ...rest) {
if (arity <= rest.length) {
return fn(...rest);
}
return curry.bind(null, arity, fn, ...rest);
}
const Into = {
array: curry(2, function(transducer, collection) {
const combine = (state, value) => (state.push(value), state);
return transduce(combine, [], transducer, collection);
}),
string: curry(2, function(transducer, collection) {
const combine = (state, value) => state.concat(value);
return transduce(combine, "", transducer, collection)
}),
set: curry(2, function(transducer, collection) {
const combine = (state, value) => state.add(value);
return transduce(combine, new Set(), transducer, collection);
}),
};
Ahora podemos usar aplicaciÃģn parcial en los argumentos. En esta ocasiÃģn logramos ese efecto con la funciÃģn curry. Vamos a probar.
const data = [-2, -1, 0, 1, 2, 3];
const transducer = compose(
filter(is_positive),
filter(is_even),
map(add_message)
);
Into.array(transducer, data);
// Array [ "The number is: 2" ]
TambiÃĐn podemos hacer esto.
const some_process = Into.array(compose(
filter(is_positive),
filter(is_even),
map(add_message)
));
some_process(data);
// Array [ "The number is: 2" ]
Pueden visualizar todo el cÃģdigo de este ejemplo aquÃ
Ahora poseemos "operaciones" reusables. No tuvimos que implementar un filter especial para el Array y otra para el Set. En este ejemplo no parece gran cosa, pero imagÃnense tener un arsenal de operaciones como RxJS, y poder usarlas en diferentes estructuras. Lo Único que deben hacer es una funciÃģn reduce. AdemÃĄs, la manera en la que combinamos estas operaciones nos invita a resolver nuestros problemas con una funciÃģn a la vez.
Hay una cosa mÃĄs que deben saber.
Esta no es su forma final
Hasta ahora he estado presentando los transductores como funciones que retornan un reducer, pero sÃģlo era para ilustrar su funcionamiento. El problema es que nuestros transductores son limitados. Hay un par de cosas que nuestra implementaciÃģn no soporta:
Mecanismo de inicializaciÃģn: Una manera de que un transductor pueda producir el valor inicial para el proceso.
InterrupciÃģn temprana: Un transductor debe ser capaz de interrumpir todo el proceso y devolver el resultado que se ha procesado hasta el momento. Algo asà como el
breakde un ciclofor.Una funciÃģn "final": BÃĄsicamente proveer un mecanismo para ejecutar una funciÃģn al final del proceso. Esto podrÃa ser Útil para ejecutar procesos de "limpieza".
Es por cosas como esas que muchos artÃculos que hablan sobre transductores recomiendan encarecidamente que usen una librerÃa.
LibrerÃas que tienen soporte para transductores solo conozco:
Siguiendo el protocolo
Ya sabemos cÃģmo funcionan los transductores a grandes rasgos, ahora vamos a descubrir cÃģmo implementar uno de la manera correcta. Para esto vamos a seguir el protocolo establecido en la librerÃa transducers-js.
Las reglas dicen que un transductor debe ser un objeto con la siguiente forma.
const transducer = {
'@@transducer/init': function() {
return /* ???? */;
},
'@@transducer/result': function(state) {
return state;
},
'@@transducer/step': function(state, value) {
// ???
}
};
@@transducer/init: SerÃĄ la funciÃģn que nos da la oportunidad de retornar un valor inicial si por alguna razÃģn necesitamos uno. El comportamiento "por defecto" es delegar sus funciones al siguiente transductor de la composiciÃģn, con suerte alguno tendrÃĄ que devolver algo Útil.
@@transducer/result: SerÃĄ la funciÃģn que se ejecute al final del proceso, es decir cuando ya no haya mÃĄs valores para procesar. Al igual que
@@transducer/init, el comportamiento que se espera por defecto es delegar sus funciones al siguiente transductor en la composiciÃģn.@@transducer/step: Aquà es donde reside la lÃģgica para nuestro transductor, es decir la "operaciÃģn" que queremos ejecutar. BÃĄsicamente esta funciÃģn serÃĄ nuestro
reducer.
AÚn no hemos terminado, tambiÃĐn necesitamos una manera de seÃąalar que el proceso serÃĄ interrumpido y regresar el resultado que se tiene en ese momento. Para esto el protocolo indica la existencia de un objeto especial que llama reduced (reducido). La idea es que cuando la funciÃģn reduce detecte este objeto se de por terminado el proceso. Este objeto debe tener la siguiente forma.
const reduced = {
'@@transducer/reduced': true,
'@@transducer/value': algo // el valor procesado hasta el momento
};
Un verdadero transducer
Es momento de aplicar todo lo que hemos aprendido, vamos a reimplementar filter de la manera correcta. Podemos hacerlo, la mayor parte va a ser igual.
Empezamos con una funciÃģn que retorna un objeto.
function filter(predicate, next) {
return {
};
}
Ahora la inicializaciÃģn, ÂŋQuÃĐ necesitamos hacer? Nada, en realidad. Entonces lo que haremos serÃĄ delegar.
function filter(predicate, next) {
return {
+ '@@transducer/init': function() {
+ return next['@@transducer/init']();
+ },
};
}
Al finalizar, ÂŋQuÃĐ necesitamos hacer? Nada. Ya saben el procedimiento.
function filter(predicate, next) {
return {
'@@transducer/init': function() {
return next['@@transducer/init']();
},
+ '@@transducer/result': function(state) {
+ return next['@@transducer/result'](state);
+ },
};
}
Ahora para el gran final, la operaciÃģn en sÃ.
function filter(predicate, next) {
return {
'@@transducer/init': function() {
return next['@@transducer/init']();
},
'@@transducer/result': function(state) {
return next['@@transducer/result'](state);
},
+ '@@transducer/step': function(state, value) {
+ if(predicate(value)) {
+ return next['@@transducer/step'](state, value);
+ }
+
+ return state;
+ },
};
}
Y que no se les olvide el toque mÃĄgico.
function filter(predicate, next) {
+ if(arguments.length === 1) {
+ return (_next) => filter(predicate, _next);
+ }
return {
'@@transducer/init': function() {
return next['@@transducer/init']();
},
'@@transducer/result': function(state) {
return next['@@transducer/result'](state);
},
'@@transducer/step': function(state, value) {
if(predicate(value)) {
return next['@@transducer/step'](state, value);
}
return state;
},
};
}
Ya tenemos el transductor, pero ahora tenemos un problema: no tenemos una funciÃģn reduce capaz de utilizarlo.
reduce mejorado
Ahora nos toca hacerle unos ajustes a nuestro reduce.
Recuerdan esto.
function reduce(reducer, initial, collection) {
let state = initial;
for(let value of collection) {
state = reducer(state, value);
}
return state;
}
Primero vamos a manejar la inicializaciÃģn.
- function reduce(reducer, initial, collection) {
+ function reduce(transducer, initial, collection) {
+ if(arguments.length === 2) {
+ collection = initial;
+ initial = transducer['@@transducer/init']();
+ }
+
let state = initial;
for(let value of collection) {
state = reducer(state, value);
}
return state;
}
Cuando la funciÃģn reciba dos argumentos la colecciÃģn estarÃĄ en initial y collection serÃĄ undefined, asà que lo que hacemos es asignar initial a collection y darle la oportunidad a nuestro transductor de generar el estado inicial del proceso.
Ahora veremos cÃģmo ejecutar el reducer que como saben ahora estÃĄ situado en @@transducer/step.
function reduce(transducer, initial, collection) {
if(arguments.length === 2) {
collection = initial;
initial = transducer['@@transducer/init']();
}
let state = initial;
for(let value of collection) {
- state = reducer(state, value);
+ state = transducer['@@transducer/step'](state, value);
}
return state;
}
Lo siguiente serÃĄ evaluar el resultado del reducer y determinar si debemos seguir con el proceso.
function reduce(transducer, initial, collection) {
if(arguments.length === 2) {
collection = initial;
initial = transducer['@@transducer/init']();
}
let state = initial;
for(let value of collection) {
state = transducer['@@transducer/step'](state, value);
+
+ if(state != null && state['@@transducer/reduced']) {
+ state = state['@@transducer/value'];
+ break;
+ }
}
return state;
}
Por Último debemos asegurarnos que todas las operaciones sepan que el proceso ha terminado.
function reduce(transducer, initial, collection) {
if(arguments.length === 2) {
collection = initial;
initial = transducer['@@transducer/init']();
}
let state = initial;
for(let value of collection) {
state = transducer['@@transducer/step'](state, value);
if(state != null && state['@@transducer/reduced']) {
state = state['@@transducer/value'];
break;
}
}
-
- return state;
+ return transducer['@@transducer/result'](state);
}
Hay un paso extra que me gustarÃa hacer. Tal vez notaron que renombrÃĐ reducer a transducer, pero me gustarÃa que siguiera funcionando con reducers normales, como los que se usa con Array.reduce. Entonces lo que haremos serÃĄ crear un transductor que pueda transformar un reducer en un transductor.
function to_transducer(reducer) {
if(typeof reducer['@@transducer/step'] == 'function') {
return reducer;
}
return {
'@@transducer/init': function() {
throw new Error('Method not implemented');
},
'@@transducer/result': function(state) {
return state;
},
'@@transducer/step': function(state, value) {
return reducer(state, value);
}
};
}
Ahora podemos usarla en reduce.
function reduce(transducer, initial, collection) {
+ transducer = to_transducer(transducer);
+
if(arguments.length === 2) {
collection = initial;
initial = transducer['@@transducer/init']();
}
let state = initial;
for(let value of collection) {
state = transducer['@@transducer/step'](state, value);
if(state != null && state['@@transducer/reduced']) {
state = state['@@transducer/value'];
break;
}
}
return transducer['@@transducer/result'](state);
}
Es momento de probar todo el arduo trabajo.
const is_positive = number => number > 0;
const data = [-2, -1, 0, 1, 2, 3];
const combine = (state, value) => (state.push(value), state);
reduce(filter(is_positive, to_transducer(combine)), [], data);
// Array(3) [ 1, 2, 3 ]
Bien, todo funciona. Pero es mucho trabajo usar reduce. Es por eso que tenemos la funciÃģn transduce, pero justo ahora le falta algo, tenemos que agregarle to_transducer.
function transduce(combine, initial, transducer, collection) {
return reduce(
transducer(to_transducer(combine)),
initial,
collection
);
}
Vamos de nuevo.
const is_positive = number => number > 0;
const data = [-2, -1, 0, 1, 2, 3];
const combine = (state, value) => (state.push(value), state);
transduce(combine, [], filter(is_positive), data);
// Array(3) [ 1, 2, 3 ]
Ahora vamos a probar la composiciÃģn.
const is_even = number => number % 2 === 0;
const is_positive = number => number > 0;
const data = [-2, -1, 0, 1, 2, 3];
const combine = (state, value) => (state.push(value), state);
const transducer = compose(filter(is_positive), filter(is_even));
transduce(combine, [], transducer, data);
// Array [ 2 ]
Pueden visualizar todo el cÃģdigo de este ejemplo aquÃ
Oficialmente hemos terminado. No hay nada mÃĄs quÃĐ hacer. Creo que ya tienen suficiente informaciÃģn para crear sus propios transductores.
ConclusiÃģn
ÂĄLo lograron! Llegaron al final del artÃculo. Debo felicitarlos, especialmente si entendieron todo en el primer intento, este no fue nada fÃĄcil. Celebren, se lo merecen.
En fin, hoy aprendimos que los transductores (en javascript) son transformaciones que pueden operar en diferentes tipos de colecciones, siempre y cuando estas provean una funciÃģn reduce que sea compatible. TambiÃĐn tienen algunas propiedades sumamente Útiles como interrupciÃģn temprana (como la de un ciclo for), mecanismos para seÃąalar la finalizaciÃģn e inicio de un proceso y pueden ser combinadas usando composiciÃģn de funciones. Y por Último, tambiÃĐn deberÃan ser eficientes, pero no son mÃĄs rÃĄpidos que un ciclo for. Aunque no sean la soluciÃģn mÃĄs eficiente en cuestiÃģn de desempeÃąo su nivel de compatibilidad con diferentes colecciones y la forma declarativa para combinar operaciones hacen que sea una herramienta poderosa.
Fuentes
- Functional-Light JavaScript | Appendix A: Transducing
- Transducers: Supercharge your functional JavaScript
- Magical, Mystical JavaScript Transducers
- Transducers: Efficient Data Processing Pipelines in JavaScript
- "Transducers" by Rich Hickey (video)
- transducers-js
Gracias por su tiempo. Si este artÃculo les pareciÃģ Útil y quieren apoyar mis esfuerzos para crear mÃĄs contenido pueden dejar una propina en ko-fi.com/vonheikemen.

Top comments (0)