LInQ in Javascript: LInQer!
Major Update
I've rewritten the entire thing into a Typescript library. I've abandoned the separation into three different Javascript files. Now it is only one having everything you need.
I haven't tested it in the wild, but you can get the new version here:
Github: https://github.com/Siderite/LInQer-ts
NPM: https://www.npmjs.com/package/@siderite/linqer-ts
Documentation online: https://siderite.github.io/LInQer-ts
Using the library in the browser directly: https://siderite.github.io/LInQer-ts/lib/LInQer.js and https://siderite.github.io/LInQer-ts/lib/LInQer.min.js
Please give me all the feedback you can! I would really love to hear from people who use this in:
- Typescript
- Node.js
- browser web sites
And now for the old post
Update: The npm package can now be used in Node.js and Typescript (with Intellisense). Huge optimizations on the sorting and new extra functions published.
Update: Due to popular demand, I've published the package on npm at https://www.npmjs.com/package/@siderite/linqer. Also, while at it, I've moved the entire code to Typescript, fixed a few issues and also create the Linqer.Slim.js file, that only holds the most common methods. The minimized version can be found at https://siderite.github.io/LInQer/LInQer.slim.min.js and again you can use it freely on your websites. Now, I hope you can use it in Node JS as well, I have no idea how to test it and frankly I don't want to. I've also added a tests.slim.html that only tests the slim version of Linqer. This version is now official and I will try to limit any breaking changes from now on. WARNING: Now the type is Linqer.Enumerable, not just Enumerable as before. If you are already using it in code, just do const Enumerable = Linqer.Enumerable;
and you are set to go.
Update: LInQer can now be found published at https://siderite.github.io/LInQer/LInQer.min.js and https://siderite.github.io/LInQer/LInQer.extra.min.js and you can freely use it in your web sites. Source code on GitHub.
Update: All the methods in Enumerable are now implemented, and some extra ones as well. Optimizations have been developed for operations like orderBy().take() and .count().
This entire thing started from a blog post that explained something that seems obvious in retrospect, but you have to actually think about it to realise it: Array functions in Javascript may seem similar to LInQ methods in C#, but they work with arrays! Every time you filter or you map something you create a new array! The post suggested creating functions that would use the modern Javascript features of iterators and generator functions and supplant the standard Array functions. It was brilliant!
And then the post abruptly veered and swerved into a tree. The author suggested we add the pipe operator to Javascript, just like they have in functional programming languages, so that we can use those static functions with hash characters as placeholders and... ugh! It was horrid! So I decided to solve the problem in a way that was more compatible with my own sensibilities: make it work just like LInQ (or at least like the Java streams).
This is how LInQer was born. You can find the sources on GitHub at /Siderite/LInQer. The library introduces a class called Enumerable, which can wrap any iterable (meaning arrays, but also generator functions or anything that supports for...of), then expose the methods that the Enumerable class in .NET exposes. There is even a unit test file called tests.html. Open it in the browser and see the supported methods. As you can see in the image of the blog post, the same logic (filtering, mapping and slicing a 10 million item array) took 2000ms using standard Array functions and 150ms using the Enumerable class.
"But wait! It says there that Enumerable exposes static methods! What exactly did you improve?", you will ask. In .NET we have something called "extension methods", which are indeed static methods that the language understands apply to specific classes or interfaces and allows you to call them like you would instance methods. So a method that looks like public static int Sum(this IEnumerable<int> enumerable)
can be used statically Enumerable.Sum(arr)
or like an instance method arr.Sum()
. It's syntactic sugar. But enough about C#, in Javascript I've implemented Enumerable as a wrapper, as Javascript doesn't know about interfaces or static extension methods. This class then exposes prototype functions that work the same way as in LInQ.
Here is where the magic happens:
Enumerable.prototype = {
[Symbol.iterator]() {
const iterator = this._src[Symbol.iterator].bind(this._src);
return iterator();
},
In other words I wrap _src (the original iterable object) in my Enumerable by exposing the same iterator for both! Now both initial object and its wrapper can be used in for...of loops. The difference is that the wrapper now exposes all that juicy functionality. Let's see a simple example: select (which is the logical equivalent of Array.map):
select(op) {
_ensureFunction(op);
const gen = function* () {
for (const item of this) {
yield op(item);
}
}.bind(this);
return new Enumerable(gen());
},
Here I am constructing a generator function which I bind to the Enumerable instance so that inside it 'this' is always that instance. Then I am returning the wrapper over the generator. In the function I am yielding the result of the op function on each item. Because of how generator functions work, only while iterating will it need to do its work, meaning that if I use the Enumerable into a for...of loop and breaking after three items, the op function will only be applied on those items, not on all of them. In order to use the Enumerable object in regular code, that works with arrays, you just use .toArray
and you are done!
Let's see the code in the tests used to compare the standard Array function use with Enumerable:
// this is the large array we are using in both code blocks:
const largeArray = Array(10000000).fill(10);
// Standard Array functions
const someCalculation = largeArray
.filter(x=>x===10)
.map(x=>'v'+x)
.slice(100,110);
// Enumerable
const someCalculation = Linqer.Enumerable.from(largeArray)
.where(x=>x===10)
.select(x=>'v'+x)
.skip(100)
.take(10)
.toArray();
There are differences from the C# Enumerable, of course. Some things don't make sense, like thenBy
after orderBy
. It can be done, but it's complicated and in my career I've only used thenBy twice! toDictionary
, toHashSet
, toLookup
, toList
have no meaning in Javascript. Use instead toMap
, toSet
, toObject
, toArray
. Last, but not least... join
. Join is so complicated to use in LInQ that I almost never used it. Also, joining is usually done in the database, so I rarely needed it. I didn't see a point in implementing it. Cast and AsEnumerable also didn't make sense. But I implemented them anyway! :) Cast and OfType are using either a class or a string to determine if an item is "of type" and join works just like in C#.
But don't fret. Any function that you want to add to this can simply be added by your own code into Enumerable.prototype
! So if you really need something custom, it's easy to add without modifying the original library.
There is one disadvantage for the prototype approach and the reason why the initial article suggested to use standalone functions: tree-shaking, the fancy word used to express the automatic elimination of unused code. But I think there is a solution for that, one that I won't implement since I believe the library is small enough and minified and compressed it will be very small indeed. The solution would involve separating each of the LINQ methods (or categories of methods, like first, firstOrDefault, last and lastOrDefault, for example) in different files. Then you can use only what you need and the files would attach the functions to the Enumerable prototype.
In fact, there are a lot of LINQ methods I have never had use for, stuff like intersect and except or append and prepend. It makes sense to create a core Enumerable class and then add other stuff only when required. But as I said, it's small enough to not matter. As such I separated the functionality in Typescript files that contain the basic functionality, the complete functionality and some extra functions. The Javascript result is a Linqer, a Linqer.slim, a Linqer.extra and a Linqer.all, covering the entire spectrum of needs. It would have been user unfriendly to put every little thing in its own file.
Bottom line, I hope you find this library useful or at least it inspires you as it did me when I've read the original blog post from Dan Shappir.
P.S. I am not the only one that had this idea, you might also want to look at linq.js which uses a completely different approach, using regular expressions. I think the closest to what LINQ should be is Manipula, that uses custom iterators for each operation rather than use the same class. Another possibility is linq-collections, which does the work via TypeScript, apparently. Also linq.es6... I am sure there are more examples if one looks closely. Feel free to let me know if you did or know of similar work and also please give me whatever feedback you see fit. I would like this library to be useful, not just a code example for my blog.