The last blog in my functional programming series (will come back to this one day) is about functors.
A functor is simply something that can be mapped over.
“Something” is simply a set of values arranged in some shape. And ‘that can be mapped over’ means that for all the values in “something”, we can do something to it (call a function). And the resulting values will be back into a new container of the same structure and shape.
class Functor {
constructor (value) {
this.value = value
}
map (f) {
return new Functor(f(this.value))
}
}
However, for a lot JavaScript objects, we can’t just map a function over it, because JavaScript objects usually don’t have a .map(f) method. We need to have a wrapper around it instead.
class Functor {
constructor (object) {
this.object = object
}
map (f) {
const mapped = { }
for (const key of Object.keys(this.object)) {
mapped[key] = f(this.object[key])
}
return new Functor(mapped)
//put it in a new wrapper that wraps an object of the same shape
}
}
Similarly we can do it using the built-in Function.prototype:
Function.prototype.map = function (f) {
const g = this
return function () {
return f(g.apply(this, arguments))
}
}
There are two key laws for functors:
- The identity law:
functor.map(x => x) == functor
This means if the function inside the functor mapping applied simply return the input, then the resulting output should be the same to the original functor:
console.log([ 0, 1, 2, 3 ].map(x => x))
// => [ 0, 1, 2, 3 ]
- The composition law:
functor.map(x => f(g(x))) ≡ functor.map(g).map(f)
You may notice that functor is just another way of composition.
Finally, as usual, just a few examples:
const Box = (x) => ({
map: (f) => Box(f(x)),
fold: (f) => f(x),
toString: () => `Box(${x})`,
chain: (f) => f(x),
});
// Exercise: Box
// Goal: Refactor each example using Box
// Keep these tests passing!
// Bonus points: no curly braces
//Ex1: Using Box, refactor moneyToFloat to be unnested.
// =========================
// const moneyToFloat = str =>
// parseFloat(str.replace(/\$/, ''))
const moneyToFloat = (str) =>
Box(str)
.map((s) => s.replace(/\$/, ""))
.map((s) => parseFloat(s))
.fold((s) => s);
// Ex2: Using Box, refactor percentToFloat to remove assignment
// =========================
const percentToFloat = (str) => {
const float = parseFloat(str.replace(/\%/, ""));
return float * 0.01;
};
// Ex3: Using Box, refactor applyDiscount (hint: each variable introduces a new Box)
// =========================
// const applyDiscount = (price, discount) => {
// const cents = moneyToFloat(price)
// const savings = percentToFloat(discount)
// return cents - (cents * savings)
// }
// const applyDiscount = (price, discount) =>
// Box(moneyToFloat(price)).fold((cents) =>
// Box(percentToFloat(discount)).fold((savings) => cents - cents * savings)
// );
const applyDiscount = (price, discount) =>
Box(moneyToFloat(price)).chain((cents) =>
Box(percentToFloat(discount)).fold((savings) => cents - cents * savings)
);