Transducers
with ECMAScript
Septiembre 2015
@gruizdevilla
Meter y sacar de contenedores tiene un precio.
Y existe un límite a lo que podemos mover de una tacada.
La solución: cadena de montaje.
¿Qué son los transducers?
- extraen la esencia de map, filter y demás
- de las funciones que transforman arrays
- para usarlas en otros sitios
- y así podemos redefinirlo como un proceso de transformaciones
¿Como se define el proceso?
- Como una sucesión de pasos
- Donde cada paso recibe una entrada
- Construir una colección es algo que se hace UNA vez
- Y se puede generalizar como una reducción por la izquierda con una semilla.
TransDucer
Transforma y reduce
Repasemos operaciones típicas sobre colecciones
Reduce
Map
Filter
const filter =
(fn, xs) =>
reduce(
(acc, x) => fn(x) ? acc.concat(x) : acc,
[],
xs
)
const map =
(fn, xs) =>
reduce(
(acc, x) => acc.concat(fn(x)),
[],
xs
)
Take
Curry
const _curry =
(fn, oldArgs) =>
(...args) => {
let allArgs = [...oldArgs, ...args];
return allArgs.length >= fn.length ? fn(...allArgs)
: _curry(fn, allArgs);
}
const curry = fn => _curry(fn, []);
Sequence
Componiendo transformaciones
const sequence =
(fn1,...fns) =>
(...args) => reduce(
(acc, fn) => fn(acc),
fn1(args),
fns
)
const transf = sequence(filter(greaterThan2), map(add2), take(3));
let result = transf([1,2,3,4,5,6]);
[1, 2, 3, 4, 5, 6] -> [3, 4, 5, 6] -> [5, 6, 7, 8] -> [5, 6, 7]
Limitaciones
- Arrays intermedios
- Los datos los proporciona un array
- Almacenamos en un array final
Nos gustaría
- No tener contenedores intermedios
- No tener como origen de datos necesariamente un array
- Poder almacenar al final donde queramos
1. Para no tener contenedores intermedios...
...no deberíamos concatenar explícitamente y en su lugar pasar el dato a la siguiente operación
2. Para no tener como origen un array...
... deberíamos permitir al origen de datos realizar la reducción.
3. Para poder almacenar donde queramos...
...debemos permitir:
- definir el contenedor inicial de acumulación
- definir como se acumula cada elemento
Pasito a pasito, que ya nos lanzaremos
Reduce: lo más sencillos
Despachamos a la implementación del objeto, si está disponible
const _reduce =
(fn, acc, [x, ...xs]) =>
x === undefined ? acc : reduce(fn, fn(acc, x), xs);
const reduce =
(fn, acc, xs) =>
hasMethod('reduce', xs) ? xs.reduce(fn, acc)
: _reduce(fn, acc, xs);
Por ejemplo, con una Lista
class Cons {
constructor(head, tail){
this.head = head;
this.tail = tail;
}
reduce(fn, acc){
return this.tail.reduce(fn, fn(acc, this.head))
}
}
const Nil = {
reduce: (fn, acc) => acc
}
const cons = (head, tail) => new Cons(head, tail);
const list = cons(1, cons(2, cons(3, Nil)));
console.log(reduce(add, 0, list)); //returns 6
Evitando las acumulaciones intermedias
const filter =
(fn, xs) =>
reduce(
(acc, x) => fn(x) ? acc.concat(x) : acc,
[],
xs
)
const map =
(fn, xs) =>
reduce(
(acc, x) => acc.concat(fn(x)),
[],
xs
)
Cosas que se hacen de más
- Llamar explícitamente a la reducción
- Concatenar explícitamente
- Definir el inicio
Saquemos la esencia de las funciones
const map =
(fn) =>
(concat) =>
xs =>
reduce(
(acc, x) => concat(acc, fn(x)),
[],
xs
)
map(x => x * 10)(append)([1,2,3]); // [10, 20, 30]
const append =
(xs, x) =>
xs.concat(x);
Paso 1: agregando
const filter =
(fn) =>
(concat) =>
xs =>
reduce(
(acc, x) => fn(x) ? concat(acc, x) : acc,
[],
xs
)
filter(x => x < 3)(append)([1,2,3]) // [1, 2]
const append =
(xs, x) =>
xs.concat(x);
Al abstraernos de la concatenación ocurre esto:
const arr = [1,2,3];
const list = cons(1, cons(2, cons(3, Nil)));
const append = (acc, item) => acc.concat(item);
const appendList = (acc, item) => cons(item, acc);
const not2 = x => x != 2;
filter(not2)(append)(list); // [1, 3]
filter(not2)(appendList)(arr); //{"head":3,"tail":{"head":1,"tail":[]}}
Casi funciona, pero el valor inicial de acumulación es "[]" y debería haber sido Nil para la acumulación en lista.
Paso 2: saquemos la reducción y el valor inicial
const mapper =
fn =>
concat =>
(acc, item) =>
concat(acc, fn(item));
const list = cons(1, cons(2, cons(3, Nil)));
const arr = [1,2,3];
const identity = x => x;
reduce(mapper(identity)(append), [], list);
//[1,2,3]
reduce(mapper(identity)(appendList), Nil, arr);
//{"head":3,"tail":{"head":2,"tail":{"head":1,"tail":{}}}}
const list = cons(1, cons(2, cons(3, Nil)));
const arr = [1,2,3];
const identity = x => x;
reduce(mapper(identity)(append), [], list);
//[1,2,3]
reduce(mapper(identity)(appendList), Nil, arr);
//{"head":3,"tail":{"head":2,"tail":{"head":1,"tail":{}}}}
const mult10 = x => x * 10;
reduce(mapper(mult10)(append), [], list);
//[10,20,30]
Mapper se ha quedado con la esencia
const mapper =
fn =>
concat =>
(acc, item) =>
concat(acc, fn(item));
Y Filterer también
const filterer =
fn =>
concat =>
(acc, item) =>
fn(item) ? concat(acc, item) : acc;
const arr = [1,2,3];
reduce(filterer(not2)(appendList), Nil, arr);
//{"head":3,"tail":{"head":1,"tail":{}}}
¿Puede "mapper" recibir otro "mapper" para concatenar?
reduce(
mapper(add1)(
mapper(mult10)
(append)
),
[],
list
)
//[20,30,40]
reduce(
filterer(not2)(
mapper(add1)(
mapper(mult10)
(append)
)
),
[],
list
)
//[20, 40]
Secuencia:
sequence(f1, f2, f3)(x) = f3(f2(f1(x)));
Composición:
(f1○f2○f3)(x) = compose(f1, f2, f3)(x) = f1(f2(f3(x)));
const sequence =
(fn1,...fns) =>
(...args) =>
reduce(
(acc, fn) => fn(acc),
fn1(...args),
fns
);
const compose =
(...args) =>
sequence(...args.reverse());
const transformation = compose(
filterer(not2),
mapper(add1),
mapper(mult10)
)
reduce(
transformation(append),
[],
list
)
//[20,40]
reduce(
transformation(appendList),
Nil,
arr
)
//{"head":40,"tail":{"head":20,"tail":Nil}}
Componemos transformaciones de forma agnóstica a la entrada y a la salida
Esto es lo que debería hacer una función "transduce"
reduce( // se encarga de reducir
transformation(append), // le decimos cual es la transformación
// y la forma de reducir
[], // le decimos el valor inicial
list // le decimos cual es la fuente de datos
)
transduce
const tranduce =
(transform, step, init, xs) =>
reduce(transform(step), init, xs);
"transduce" no maneja estados, pero los transformers pueden tenerlo, como "taker"
const taker =
n =>
concat =>
(
(pos = 0) =>
(acc, item) => (pos++, pos <= n ? concat(acc, item) : acc)
)()
transduce(transform, step, init, xs)
- "trans"(transform): ejecuta una serie de transformaciones
- "duce"(step): reduce de la forma que se le diga
- (init): comenzando con un valor
- (xs): los datos que le informemos
protocolos:
transducers
transformers
reduced
iterator (ya visto en curso de ES2015)
transducer
Una función de grado/aridad uno, que recibe un tranformer para poder componer.
const map =
fn =>
xf =>
new Mapper(fn, xf);
las implementaciones de las librerías pueden permitir funcionar con o sin transducers
const map =
fn =>
xf =>
isTransformer(xf) ? new Mapper(fn, xf)
: _plainOldMap(fn, xf);
Pero para ello necesitamos saber si "xf" es un transformer.
transformer
- para cumplir con el protocolo
debe implementar los siguientes métodos:- [@@transducer/init]
- [@@transducer/result]
- [@@transducer/step]
class Mapper extends Base {
constructor(fn, xf) {
super();
this.xf = xf;
this.f = fn;
}
'@@transducer/step'(result, input) {
return this.xf['@@transducer/step'](result, this.f(input));
}
'@@transducer/init'() {
return this.xf['@@transducer/init']();
}
'@@transducer/result'(result) {
return this.xf['@@transducer/result'](result);
}
}
De esta forma componemos transformaciones de forma habitual
const transformation = compose(
filter(isOdd),
map(addTen),
map(prop('somprop'))
);
transformation([{obj1},{obj2},{obj3},...])
into([], transformation ,someReducibleObject)
Aviso a navegantes
COMPOSE:
- normalmente de derecha a izquierda
- en un transducer de izquierda a derecha
reduced
- a veces queremos cortar una reducción
- para cumplir con el protocolo, el valor
debe implementar las siguientes propiedades:- [@@transducer/reduced]
- [@@transducer/value]
const arrayReduce =
(xf, acc, arr, pos, length) =>
acc && acc['@@transducer/reduced'] ? acc['@@transducer/value'] :
length === pos ? xf['@@transducer/result'](acc)
: arrayReduce(xf,
xf['@@transducer/step'](acc, arr[pos]),
arr,
pos + 1,
length);
Como quedaría un parse de logs, por ejemplo (usando RamdaJS):
//pagevisits.js
var lines = require('transduce/string/lines')
var stream = require('transduce-stream')
var parseLog = R.compose(
lines(),
R.filter(isPage),
R.map(splitLine),
R.map(valueToUrl),
R.map(R.join(' visited ')),
R.map(R.add(R.__, '\n')))
process.stdin.pipe(stream(parseLog)).pipe(process.stdout)
process.stdin.resume()
# Term 1
$ tail -f access.log | node pagevisits.js
127.0.0.1 visited http://localhost/path1/
127.0.0.1 visited http://localhost/path1/
¡Gracias!
Transducers with ECMAScript
By Gonzalo Ruiz de Villa
Transducers with ECMAScript
- 3,307