LINQ-like functions in JavaScript with deferred execution

Let’s admit it: as C# developers, we are quite lucky. Not only the language is well-designed, but it also keep evolving and getting amazing new features. Moreover, Microsoft has changed the release strategy, so that we get both stable new versions and the “point releases”. The current stable major release is C# 7.0, but you can already use 7.1 and 7.2 is coming soon. Also, everything is now happening in the open, so here we can see the C# language roadmap and feature status.

LINQ in C#

One of the features that probably makes programmers in other languages jealous is Language Integrated Query. It allows to compose multiple filtering/aggregating/mapping steps when processing collections in a very readable and functional way. As an illustration, here’s a piece of code that accepts an IEnumerable<int> and returns an array of only even numbers squared:

static class Foo
{
    public static int[] GetSquaredEvens(IEnumerable<int> numbers)
    {
        var query = from number in numbers
                    where number % 2 == 0
                    select number * number;

        return query.ToArray();
    }
}

Of course, these magic from, where, and select keywords are just syntactic sugar and will be turned by compiler into actual static methods calls of System.Linq.Enumerable type. So, the same code can be rewritten as a sequence of chained extension methods:

public static int[] GetSquaredEvens(IEnumerable<int> numbers)
{
    var query = numbers
                .Where(number => number % 2 == 0)
                .Select(number => number * number);

    return query.ToArray();
}

The reason I’m first creating a query variable and only then calling ToArray() method is to emphasize that the filtering and squaring won’t happen until the IEnumerable<int> is actually enumerated. That is, by the time the var query = ... line has been executed, nothing happened yet, the LINQ logic was only prepared for execution.

I can imagine this is nothing new for the majority of C# developers out there, since we’ve had LINQ since 2007. What triggered me to write about this is a job interview I have recently had (on the interviewer side), where the candidate mentioned that he wrote an open source JavaScript library implementing some LINQ functions. It was written a while ago, when JavaScript didn’t have arrow functions yet. Some of the similar methods, like Array.prototype.map() and Array.prototype.filter(), have been first implemented in ECMAScript 5.1 standard published in 2011. So, we can easily come up with a JavaScript function very similar to our C# version:

function getSquaredEvens(numbers) {
    var results = numbers
                .filter(number => number % 2 === 0)
                .map(number => number * number);

    return results; // no ToArray-like stuff, already enumerated
}

Looks very close to C# version, right? A big difference, however, is that filter and map functions are immediately executed, so the question I asked the interviewee (and myself) was about how we could implement such LINQ-like functions in JavaScript with deferred execution like in C#.

Deffered execution in JavaScript?..

Luckily, JavaScript has been evolving too, so since ECMAScript 2015 there is a concept of generators. It should, once again, seem very familiar to C# developers, since it is based on the yield keyword and allows to define function with custom iteration behaviour. Here is the simplest example from Mozilla documentation:

function* idMaker() {
    var index = 0;
    while(true)
        yield index++;
}

The asterisk denotes that this is not just a function, but a generator, which is a factory for iterators. Simply put, when executed, a generator function will return an iterator, which can be enumerated using for...of loops.

That is exactly what we need! Now we can extend JavaScript’s Array and Generator types with our own LINQ-like functions, so that they can be chained in any order and only get executed when iteration actually starts:

// define generator functions
const whereGenerator = function* (isMatch) {
    for (const item of this) {
        console.log('filtering!');
        if (isMatch(item)) {
            yield item;
        }
    }
};

const selectGenerator = function* (transform) {
    for (const item of this) {
        console.log('mapping!');
        yield transform(item);
    }
};

const toArrayFunction = function () {
    return Array.from(this);
};

// obtain generator prototype object
const Generator = Object.getPrototypeOf(function* () {});

// extend
Generator.prototype.where = whereGenerator
Array.prototype.where = whereGenerator

Generator.prototype.select = selectGenerator;
Array.prototype.select = selectGenerator;

Generator.prototype.toArray = toArrayFunction;
Array.prototype.toArray = toArrayFunction;

The code may look a bit funky, but the idea is simple: both Array and Generator types get extended with where/select generator functions (which, in turn, return generators too) and toArray function (this one terminates the chain, materializing the collection). This will allow us to chain these methods on both arrays and intermediate generators without actually enumerating. Only when toArray is called or results are iterated with for...of loop will generators start executing. To be able to easily check this, I have added console.log() statements to where and select generator functions (we’ll use them shortly).

Finally, we can write our filtering/squaring LINQ-like function in almost the same manner as we did in C#:

const numbers = [1,2,3,4,5,6,7,8,9]

function getSquaredEvens(numbers) {
    const query = numbers
                .where(number => number % 2 === 0)
                .select(number => number * number);

    return query;
}

var results = getSquaredEvens(numbers);
debugger;

for (const number of results) { // results.toArray() will work too
    console.log(number);
}

If you try to execute it in the browser (I was testing this code in Chrome 61 and Firefox 56), the execution should pause at the line with debugger statement. Note that by this moment getSquaredEvens will have finished already, but since there’s no toArray inside it, it only returns a chain of generators. To verify that no enumeration happened yet, you can switch to the Console tab and observe that nothing was written there:

/img/generator-debugger-console-empty.png

And if you continue the execution with F8, the for loop will start and you’ll finally see messages being written in the console:

/img/generator-debugger-console-messages.png

Now we can do LINQ in JavaScript!

More information

Obviously, this was just an exercise in understanding some advanced features of ES6, not anything resembling production-ready code. To efficiently work with collections in JavaScript, I would rather rely on mature libraries like underscore or lodash. But if you, just like me, want to learn more about languages features of ES6, I would recommend something like a ES6 couse by Wes Bos or ES6 Javascript: The Complete Developer’s Guide by Stephen Grider.

No comment

Creative Commons License