jlongster / transducers.js

A small library for generalized transformation of data (inspired by Clojure's transducers)
BSD 2-Clause "Simplified" License
1.73k stars 54 forks source link

Proposal: @@transducer/init should always be called, create an accumulator object. #46

Open shaunc opened 7 years ago

shaunc commented 7 years ago

Consider the following implementation of a "zip" transformer:

function zip() {
  return xf => Zip(xf);
}
const sub = Symbol('sub');
function Zip(xf) {
  return {
    ['@@transducer/init']() {
      const result = { [sub]: [] };
      // if init is not implemented in wrapped, ignore
      try {
        result.wrapped = xf['@@transducer/init']();
      }
      catch(err) { }
      return result;
    },
    ['@@transducer/result'](result) {
      if(result[sub] == null || result[sub].length === 0) {
        return result.wrapped || result;  
      }
      const wrappedResult = result[sub][0].reduce((acc, input, i)=>
        xf['@@transducer/step'](acc, result[sub].map((a)=>a[i]))
      , result.wrapped);
      return xf['@@transducer/result'](wrappedResult);
    },
    ['@@transducer/step'](result, input) {
      if(result[sub] == null) {
        // "into" - init not called
        const acc = this['@@transducer/init']();
        // pass the evil on to the wrapped accumulator
        acc.wrapped = result;
        result = acc;
      }
      result[sub].push(input);
      return result;
    }
  };
}

It "works" but does it by hackery. What it should do is create an accumulator object in init, then accumulate the subcollections. On finalization, it can feed the final objects to the downstream transformer.

I propose that @@transducer/init be responsible to create and return an accumulator object which wraps the downstream transformer accumulator. It could have signature:

['@@transducer/init'](finalAccumulator) { }

With default implementation:

['@@transducer/init'](finalAccumulator) { return xf['@@transducer/init](finalAccumulator); }

(Here xf is the downstream transformer -- could be this.xf depending on implementation.)

By default, as in zip, we could use the accumulator to store state. Eg. we could have a transducer that was calculating a histogram, then forwarding the bins onwards (etc.).

If an accumulator did wrap downstream state in its own state, it is then responsible for unwrapping the downstream state in step and result.

finalAccumulator is the thing into which the whole pipeline is writing. Normally we ignore it, but special end of chain "output aware" transformers could use it (provided by the library).

ScriptedAlchemy commented 5 years ago

https://github.com/ScriptedAlchemy/fast-transducers

im maintaining a fork of this and will for many years, feel free to open PR;s to me