Search code examples
javascriptecmascript-6promisefunctional-programmingmethod-chaining

Functional Programming - then() between chained filter/map calls


I am parsing data like this:

getData()
.filter(fn)
.filter(fn2)
.filter(fn3)
.map(fn4)

in which the filters are conceptually separated and do different operations.

For debugging purposes, is there a JavaScript library or a way to wrap promises such that I can do this:

getData()
.filter(fn)
.then((result) => { log(result.count); return result })
.filter(fn2)
.then(debugFn)    // extra chained debug step (not iterating through arr)
.filter(fn3)
.map(fn4)

Or is this an anti-pattern?


Solution

  • EDIT

    After some thoughts I'm convinced that the best answer to this question has been given by V-for-Vaggelis: just use breakpoints.

    If you do proper function composition then inserting a few tap calls in your pipeline is cheap, easy and non intrusive but it won't give you as much information than what a breakpoint (and knowing how to use a debugger to step through your code) would.


    Applying a function on x and returning x as is, no matter what, already has a name: tap. In libraries like , it is described as follow:

    Runs the given function with the supplied object, then returns the object.

    Since filter, map, ... all return new instances, you probably have no other choice than extending the prototype.

    We can find ways to do it in a controlled manner though. This is what I'd suggest:

    const debug = (xs) => {
      Array.prototype.tap = function (fn) {
        fn(this);
        return this;
      };
      
      Array.prototype.debugEnd = function () {
        delete Array.prototype.tap;
        delete Array.prototype.debugEnd;
        return this;
      };
     
      return xs;
    };
    
    const a = [1, 2, 3];
    
    const b =
      debug(a)
        .tap(x => console.log('Step 1', x))
        .filter(x => x % 2 === 0)
        .tap(x => console.log('Step 2', x))
        .map(x => x * 10)
        .tap(x => console.log('Step 3', x))
        .debugEnd();
    
    console.log(b);
    
    try {
      b.tap(x => console.log('WAT?!'));
    } catch (e) {
      console.log('Array prototype is "clean"');
    }

    If you can afford a library like Ramda, the safest way (IMHO) would be to introduce tap in your pipeline.

    const a = [1, 2, 3];
    
    const transform =
      pipe(
          tap(x => console.log('Step 1', x))
        , filter(x => x % 2 === 0)
        , tap(x => console.log('Step 2', x))
        , map(x => x * 10)
        , tap(x => console.log('Step 2', x))
      );
      
    console.log(transform(a));
    <script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script>
    <script>const {pipe, filter, map, tap} = R;</script>