Sunday 10 November 2019

Deferred-Lazy Iterable

When in my last post I talked about how to provide lodash-like functionality to Async Iterables, I did not realise that indeed lodash (or underscore for the matter) do not work with normal Iterables. The Array methods work with arrays and the Collection methods work with Objects. Well, Iterables are objects, but if you check the source code for example for map you'll see that it checks if the object is an array (in which case it traverses it with a while-length loop) or just an Object, in which case it traverses it using Object.keys... so it won't work for non-Array iterables. The solution is converting your iterable to an Array, which in modern JavaScript is so clean as doing [...myIterable],

var _ = require('lodash');

function* citiesGeneratorFn(){
 let cities = ["Xixon", "Toulouse", "Lisbon", "Tours"];
 for (city of cities){
  yield city;
 }
}

let cities = citiesGeneratorFn();

//lodash methods do not work with iterables
console.log("with iterable");
console.log(_.map(cities, city => city.toUpperCase()).join(";"));
//nothing gets printed

//we have to convert the iterable to an array
console.log("with array");
console.log(_.map([...cities], city => city.toUpperCase()).join(";"));
//XIXON;TOULOUSE;LISBON;TOURS

Problem with this is that you are losing one of the main features of iterables, that they are deferred. First, if my generator function*: citiesGeneratorFn above were doing something like retrieving the city data via a server call, I would not need to do that retrieval until the moment when I were doing a next call to get that city, but when converting to array, we are doing all those calls in advance, even if then I'm only going to use the first city. Second, as lodash does not support iterables, all the map, filter... functions are applied to all the elements of the array, even if again we are only going to use the first one. Because of this is why a library like lazy.js (of which I talked recently) exists.

So, it came to my mind how would I go in order to try to implement something like that, a map, filter... functions that worked nicely with iterables and hence deferred. Thanks to generators it's pretty straightforward. I create an ExtendedIterable class that will wrap the original iterable. This class features map and filter methods. The important thing is that each of these methods returns a new ExtendedIterable (so I can chain the calls) that applies the function logic to the value to return, one by one, taking advantage again of generators. All in all, we have this:

class ExtendedIterable{
    constructor(iterable){
        this.iterable = iterable;
    }

    *[Symbol.iterator](){
        for (let it of this.iterable){
            yield it;
        }
    }

    map(fn){
        //I need to use the old "self" trick, cause we can not declare a generator with an arrow function
        //so as we are using a normal function, "this" is dynamic, so we have to trap it via "self"
        let self = this;
        function* _map(fn){
            for (let it of self.iterable){
                yield fn(it);
            }
        };
        let newIterable = _map(fn);
        return new ExtendedIterable(newIterable);
    }

    filter(fn){
        let self = this;
        function* _filter(fn){
            for (let it of self.iterable){
                if (fn(it))
                    yield it;
            }
        };
        let newIterable = _filter(fn);
        return new ExtendedIterable(newIterable);
    }
}

function* citiesGeneratorFn(){
 let cities = ["Xixon", "Toulouse", "Lisbon", "Tours"];
 for (city of cities){
  yield city;
 }
}
let cities = citiesGeneratorFn();

//I could just use the array, and array is iterable and the citiesGenerator is not doing any processing, just retrieving the value directly
//let cities = ["Xixon", "Toulouse", "Lisbon", "Tours"];

let extendedIterable = new ExtendedIterable(cities);

let result = extendedIterable.map(it => it.toUpperCase())
    .filter(it => it.startsWith("T"));

for (let it of result){
    console.log(it);
} 

//or just convert to array and join it
//console.log([...result].join(";"));

I've uploaded the code to a gist. This is just a POC, if you want something real, go for lazy.js.

No comments:

Post a Comment