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

 

  1. No tener contenedores intermedios
  2. No tener como origen de datos necesariamente un array
  3. 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

  • 1,763
Loading comments...

More from Gonzalo Ruiz de Villa