Now things are just getting silly.
The second half of my blog post about array functions included a brief intro to functional composition. One thing I didn't mention was combining composed functions.
1
2
3
4
5
6
7
8
9
10
11
var greaterThan = function(num) {
return function(x) {
return x > num;
}
};
var lessThan = function(num) {
return function(x) {
return x < num;
}
};
It's possible to use these two separately...
1
[1, 2, 3].filter(lessThan(3));
...but what if we wanted to use them together to define a range?
1
[1, 2, 3].filter(greaterThan(1) *and* lessThan(3));
A simple solution would be to create a function that would combine the effects of two filtering functions into a single filter.
1
2
3
4
5
6
var bind = function() {
var fs = Array.prototype.slice.apply(arguments, [0]);
return function(x) {
return fs.every(function(f){ return f(x); });
}
};
This monad (bind) takes an unlimited list of input functions (arguments
), and turns them into an array. The function that is returned will every
through each filter function, returning the &&
of all filters;
1
2
3
4
[1, 2, 3].filter(bind(greaterThan(1), lessThan(3)));
// Output:
[2]
Having a separate function, bind
to combine functions works, but it's a bit ugly.
One solution is just to build a range
function that combines the other two.
1
2
3
4
5
6
7
var range = function(low, high) {
return bind(greaterThan(low), lessThan(high));
}
[1, 2, 3].filter(range(1, 3));
// Output: [2]
Another possible alternative syntax would be if functions could be chained together in a more natural/english sort of way. Something along the lines of...
1
[1, 2, 3].fitler(greaterThan(1).and(lessThan(3)));
To make this possible there would need to be an .and
method on whatever the filter function returned. Recall that the filter methods return a function, which means that the returned function would need to have .and
. We can start by creating a function that adds the .and
extention onto a function.
1
2
3
4
5
6
7
8
9
10
11
var $f = function(comp) {
return function() {
var f = comp.apply(f, arguments);
f.and = function(f2) {
return $f(function() {
return bind(f, f2);
})();
};
return f;
};
}
$f
is a bit of a magical function. It takes one argument, comp
which is our compositional function. comp
would typically be returning a function that would be our filter, however $f
replaces that with a function that applies the arguments to comp
, then adds the .and
property onto that composed (comp resulting) function.
In order to allow subsequent .and
's to be called, the $f
is then called on whatever .and
binds.
Then we can re-defined greaterThan
as a filter. Note the new $f()
wrapping the filters.
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
var greaterThan = $f(function(num) {
return function(x) {
return x > num;
}
});
var lessThan = $f(function(num){
return function(x) {
return x < num;
}
});
var even = $f(function(){
return function(x) {
return x % 2 == 0;
}
});
[1, 2, 3, 4, 5, 6].filter(
greaterThan(1)
.and(lessThan(5))
.and(even())
);
// Output:
// [2, 4]
Oh geeze, this is starting to look like Lisp.
It's worth pointing out that the syntax could be extended to include or
. Here's the whole clip of code.
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
// Monads
var bind = function() {
var fs = Array.prototype.slice.apply(arguments, [0]);
return function(x) {
return fs.every(function(f){ return f(x); });
}
};
var bindOr = function() {
var fs = Array.prototype.slice.apply(arguments, [0]);
return function(x) {
return fs.some(function(f){ return f(x); });
}
};
// Filter builder
var $f = function(comp) {
return function() {
var f = comp.apply(f, arguments);
f.and = function(f2) {
return $f(function() {
return bind(f, f2);
})();
};
f.or = function(f2) {
return $f(function() {
return bindOr(f, f2);
})();
};
return f;
};
}
// Filters
var greaterThan = $f(function(num) {
return function(x) {
return x > num;
}
});
var lessThan = $f(function(num){
return function(x) {
return x < num;
}
});
var even = $f(function(){
return function(x) {
return x % 2 == 0;
}
});
var equals = $f(function(num){
return function(x) {
return x === num;
}
});
// In use
[1, 2, 3, 4, 5, 6].filter(
greaterThan(1)
.and(lessThan(5))
.and(even())
.or(equals(3))
);
// Output:
// [2, 3, 4]
The clever programmer would have spotted that our monads could actually be re-factored into their own composer too...
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
// This:
var bind = function() {
var fs = Array.prototype.slice.apply(arguments, [0]);
return function(x) {
return fs.every(function(f){ return f(x); });
}
};
var bindOr = function() {
var fs = Array.prototype.slice.apply(arguments, [0]);
return function(x) {
return fs.some(function(f){ return f(x); });
}
};
// Into this:
var monad = function(filter) {
return function() {
var fs = Array.prototype.slice.apply(arguments, [0]);
return function(x) {
return fs[filter](function(f){ return f(x); });
}
}
}
var bind = monad('every');
var bindOr = monad('some');
And that, boys and girls, is getting started in the amazing world of functional programming.