I come from the .NET world, where I’m used to using LINQ for massaging
collections of data in a clear functional way.
ES6 added lots of useful functions to Array
that lets me write code like
1
| [1,2,3,4].filter(x => x ===3).map(x => x *2);
|
I like this style because it avoids deep nested loops and makes the
intention of the code very clear.
To my surprise, I learnt that I can’t write code like this for other
data structures like Map.
because these expose their data as iterators
and not as Arrays.
Iterators are a whole other beast introduced in ES6, and the only built-in way
to use filter/map with them is to convert then to Arrays.
1
2
3
4
5
6
7
| let map = new Map([['foo', 3], ['bar', 2], ['baz', 4]])
const reducer = (accumulator, currentValue) => accumulator + currentValue;
const result = Array.from(map)
.map(x => `key: ${x[0]} value: ${x[1]}`)
.reduce(reducer);
console.log(result)
|
There are serveral libraries like wu that
adds support for these operations and lots more.
Instead of just adding another library, I figured I would see how wu
worked and see if I can create something for just the parts I need.
Luckily wu
is a pretty well organized library and while my
javascript foo is not that strong,
I was able to break down the basic functionality.
Lets start by trying to create an object that acts as a proxy for iterables.
We can add methods on that object like filter and map later on.
I’ll call it Chain
just because I want to use this to chain a bunch of methods on iterators.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| class Chain {
constructor(iterable) {
this.iterator = Chain.getIterator(iterable);
}
[Symbol.iterator]() {
return this.iterator;
}
* filter(fn = Boolean) {
for (let x of this) {
if (fn(x)) {
yield x;
}
}
}
/**
* Return whether a thing is iterable.
*/
static isIterable(thing) {
return thing && typeof thing[Symbol.iterator] === 'function';
};
/**
* Get the iterator for the thing or throw an error.
* @param thing
* @return {*}
*/
static getIterator(thing) {
if (Chain.isIterable(thing)) {
return thing[Symbol.iterator]();
}
throw new TypeError('Not iterable: ' + thing);
};
}
|
This is a good starting point.
Chain
acts as a proxy for the iterable and so we can some helper methods to it like filter
You can use it like so
1
2
3
| for (const item of new Chain([1, 2, 3]).filter(x => x === 1)) {
console.log(item);
}
|
Output
I don’t want to keep typing new Chain()
every time, so I’ll add a little helper
1
2
3
| function chain(iterable) {
return new Chain(iterable);
}
|
So now I can do this
1
2
3
| for (const item of chain([1, 2, 3]).filter(x => x === 1)) {
console.log(item);
}
|
The problem is we can’t chain these together.
We need map
to return another instance of Chain
Lets modify Chain
to do that.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| class Chain {
constructor(iterable) {
this.iterator = Chain.getIterator(iterable);
}
[Symbol.iterator]() {
return this.iterator;
}
/**
* internal filter
* @param fn
* @returns {IterableIterator<*>}
* @private
*/
* _filter(fn) {
for (let x of this) {
if (fn(x)) {
yield x;
}
}
}
/**
* wrapper filter
* @param fn
* @returns {*}
*/
filter(fn = Boolean) {
return chain(this._filter(fn))
}
/**
* Return whether a thing is iterable.
*/
static isIterable(thing) {
// same as before
};
/**
* Get the iterator for the thing or throw an error.
* @param thing
* @return {*}
*/
static getIterator(thing) {
// same as before
};
}
|
What is different?
filter()
now returns the _filter()
iterable, wrapped in a Chain()
.
Now let me add a map
to Chain
.
1
2
3
4
5
6
7
8
9
| * _map(fn) {
for (let x of this) {
yield fn(x);
}
};
map(fn) {
return chain(this._map(fn));
}
|
The set up looks the same as filter.
Now we can do something like this
1
2
3
| for (const item of chain([1, 2, 3, 4]).filter(x => x % 2 === 0).map(x => x + 1)) {
console.log(item);
}
|
Output
And what about reduce?
1
2
3
4
5
6
7
8
9
10
11
12
13
| reduce(fn, initial) {
let val = initial;
let first = true;
for (let x of this) {
if (val === undefined) {
val = x;
first = false;
continue;
}
val = fn(val, x);
}
return val;
}
|
This is a simpler one, since we’re not going to return an iterator from this function.
If we are not provided with an initial value, we will use the first item in the iterator as the initial value.
Now you can do this
1
2
3
4
5
| const item2 = chain([1, 2, 3, 4])
.filter(x => x % 2 === 0)
.map(x => x + 1)
.reduce((x, y) => x + y);
console.log(`reduce: ${item2}`);
|
Output
This is what the complete class looks like
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
| class Chain {
constructor(iterable) {
this.iterator = Chain.getIterator(iterable);
}
next() {
return this.iterator.next.call(this.iterator);
}
[Symbol.iterator]() {
return this.iterator;
}
/**
* internal filter
* @param fn
* @returns {IterableIterator<*>}
* @private
*/
* _filter(fn) {
for (let x of this) {
if (fn(x)) {
yield x;
}
}
}
/**
* wrapper filter
* @param fn
* @returns {*}
*/
filter(fn = Boolean) {
return chain(this._filter(fn));
}
* _map(fn) {
for (let x of this) {
yield fn(x);
}
};
map(fn) {
return chain(this._map(fn));
}
reduce(fn, initial) {
let val = initial;
let first = true;
for (let x of this) {
if (val === undefined) {
val = x;
first = false;
continue;
}
val = fn(val, x);
}
return val;
}
/**
* Return whether a thing is iterable.
*/
static isIterable(thing) {
return thing && typeof thing[Symbol.iterator] === 'function';
};
/**
* Get the iterator for the thing or throw an error.
* @param thing
* @return {*}
*/
static getIterator(thing) {
if (Chain.isIterable(thing)) {
return thing[Symbol.iterator]();
}
throw new TypeError('Not iterable: ' + thing);
};
}
|
We can add lots more functionality to this as needed, but this is a good starting point.
Here is also a gist for the whole thing