Chain to native Javascript functions
TL; DR:
How can I connect to Javascript map () using my own function? How -
stuff.map(i => i.key).avg()
where avg () is my own function to calculate the average of the array returned by the map?
In moving away from objects and towards functional programming with pure functions, I lost the comfortable
return this;
which allows me to chain.
If I have
let stuff = [
{id: 1, name: 'tuan', country: 'VN', age: 23},
{id: 2, name: 'nhung', country: 'US', age: 25},
...
//my own filter to pass as a param to native filter()
var filt = x => j => j.country === x;
//my own reducer for an array that computes an average
let avg = (arr) => (arr.reduce((acc, i) => acc + i) / arr.length);
then
stuff.filter(filt('VN')).map(i => i.age)
will return something like
[23, 34, 45]
but
stuff.filter(filt('VN')).map(i => i.age).avg()
gives an error like
filter().map().avg() is not a function
How can we write functions that relate to native ones?
source to share
Method chaining is incompatible with function composition. But instead of modifying built-in prototypes or falling back to subtyping, you can create a container type that allows you to compose pure functions in the context of a method chaining:
function Box(x) {
return new.target ? (this.x = x, this) : new Box(x)
}
Box.prototype.fold = function fold(f) {return f(this.x)};
Box.prototype.map = function map(f) {return new Box(f(this.x))};
Box.prototype.toString = function toString() {return `Box(${this.x})`};
const id = x => x;
const stuff = [
{id: 1, name: 'foo', country: 'VN', age: 23},
{id: 2, name: 'bar', country: 'US', age: 25},
{id: 2, name: 'bat', country: 'VN', age: 34},
{id: 2, name: 'baz', country: 'VN', age: 45}
];
const filt = x => j => j.country === x;
const avg = (arr) => (arr.reduce((acc, i) => acc + i) / arr.length);
console.log(
Box(stuff.filter(filt('VN')).map(i => i.age))
.map(xs => avg(xs))
.fold(id) // yields 34
);
Box
is a functor and you can put values ββof any type in this container. With, map
you can apply functions to a value inside a functor and get a new functor with the converted value back. fold
behaves the same, except that it returns null.
You may have noticed that my example is a little verbose and I could spare me the mapping.
source to share
create avg method on Array.prototype
Array.prototype.avg = function() {
return this.reduce((a,b) => Number(a) + Number(b)) / this.length;
}
var array = [
{ id: 1, key:2 },
{ id: 2, key:3 },
{ id: 3, key:7 },
{ id: 4, key:6 },
{ id: 5, key:4 }
]
var avg = array.map(i => i.key).avg();
console.log(avg);
source to share
The chaining isn't magic - you just call the method on the return value of the function. If the function doesn't support this method, you need to add it to the prototype.
This works because it map
returns, and array and arrays have a method join()
:
var a = [1, 2, 3, 4, 5]
a.map((i) => i *2 ).join(",")
But arrays don't have a method avg()
unless you add one so the chaining doesn't work.
source to share
Well, you have some options here for sure. There is no one right way to achieve what you want. I think your best option is to extend the JavaScript Array class.
class PizzaCollection extends Array {
// .. collection specific methods here...
avg() {
// you can iterate on `this`
}
}
.map, .filter, etc. all will return an instance of PizzaCollection.
Try it!
const j = new PizzaCollection(1, 2, 3)
const k = j.map((num) => num * num)
k instanceof PizzaCollection // returns true
k.avg() // returns the avg
source to share