About a year ago, some colleagues and I paired up on creating our own functions instead of using a library to keep the bundle size down. While one option is to use tree-shaking, we figured we could reduce it even further by doing away with some of the scaffolding and boilerplate that comes along with it, and I had an idea I really wanted to try out. This is the story of how I experimented with native Javascript and flipped it inside out to a more functional paradigm, and had a functional mini-library of around 500 bytes.
Its starts with a scenario in which I dream up many of my ideas, while I was driving home. As I traveled down the interstate the thought occurred to me, “What if we created an Object that just intrinsically used the already existing Array methods, but it had currying and could use pipe and compose!”
I mean, all of the common functions are widely available — like map, reduce, filter, and so on. The only thing I really wanted to do is to somehow “borrow” their already optimized functionality, not rewrite them.
I also figured that the only functions that we didn’t have that I really wanted and to make it all work would be pipe
and compose
— which would be really the only “non-native” functions in our toolbox for this experiment.
Since Javascript’s Array
object has all of the functions I needed, I figured I could pluck them out. To do that, I created a “whitelist” of functions I wanted from the Array Object:
const whitelist = ['concat', 'every', 'filter ', 'find ', 'includes', 'keys', 'map', 'reduce ', 'reduceRight', 'slice', 'some']
Alright! So after perusing the MDN docs, and paying no mind at all to browser compatibility (at least not YET anyway!) I sought to “invert” (maybe wrong term here) the functions so that they would be curry-able, so you could do stuff with them using a function like compose
or pipe
The trick was to create a new Object, and plop the Array’s prototype methods directly onto it. The first name I came up with (which has been renamed a few times since) was Box
which was inspired by Dr. Boolean’s series on egghead
It would also use getOwnPropertyNames
from Array.prototype
, see if any of the methods are available on the prototype that match the whitelist, and if they were there, make them part of theBox
Object.
This would allow for unsupported methods to just be as they already are: undefined
— So if someone wanted to consume this Object, they were not getting anything really new, just a different way of using the same functions they already have. If you needed some additional browser support you could just add the polyfill and then add the Box
— which I just thought was a neat idea to go about it!
const Box = {}Object.getOwnPropertyNames(Array.prototype).filter(s => whitelist.includes(s)).forEach(method => {Box[method] = fn => a => a[method](fn)})
So now the only thing left was to add pipe and compose:
(special thanks to Ronn Ross for writing these handy ones)
Box.compose = (...fns) => data => fns.reduce((value, fn) => fn(value), data)[0]Box.pipe = (...fns) => data => fns.reverse().reduce((value, fn) => fn(value), data)
module.exports = Box
Alrighty, so to put this entire functional powerpack together, at a very tiny size of something around 500 bytes you have:
// Let's pluck these methods off of Array.prototypeconst whitelist = ['concat', 'every', 'filter ', 'find ', 'includes', 'keys', 'map', 'reduce ', 'reduceRight', 'slice', 'some']
// We'll put them on a container Objectconst Box = {}
// So if any of the methods exist, we add them, if not, we just move on.Object.getOwnPropertyNames(Array.prototype).filter(s => whitelist.includes(s)).forEach(method => {Box[method] = fn => a => a[method](fn)})
// We'll create two functions that are our own for making things compositionalBox.compose = (...fns) => data => fns.reduce((value, fn) => fn(value), data)[0]Box.pipe = (...fns) => data => fns.reverse().reduce((value, fn) => fn(value), data)
module.exports = Box
Pretty cool! So how do you use the compose and pipe functions?
here’s an example using compose
compose(// 42reduce((acc, val) => val + acc),// [12, 14, 16]map(x => x * 2),// [6, 7, 8]filter(x => x > 5),// [0, 1, 2, 3, 4, 5, 6, 7, 8],concat([6, 7, 8])// [0, 1, 2, 3, 4, 5])([1, 2, 3, 4, 5])
So that contrived example creates a function that we pass an array of [1, 2, 3, 4, 5] into it. The function then passes the arguments along from right to left (as compose works) — resulting in 42 :)
If we wanted to read it from left to right, we could use pipe:
pipe(map(x => x + 1),// [1]map(x => x + 1),// [2]map(x => x + 1),// [3])([0])
Like the compose method, this pipe method creates a function that takes the paramater [0]
, which is then *piped* from left to right, incrementing it by one as it goes.
You could do some fancier things with this, and you dont necessarily need to use pipe and compose, you could make a function called addTwo
which is basically a curried function that adds two to the parameter, again using our functionalized array methods:
const arrayOne = [1, 2, 3];const addTwo = concat([4, 5])const result = (addTwo(arrayOne))
console.log(result)// [1, 2, 3, 4, 5]
I’m hoping someone finds this useful, because you really can do functional programming with javascript without a library!
That being said, a library gives you extra functionality, and backwards compatibile support. You can even use tree-shaking to reduce the bundlesize, so I can’t say it enough, if you want to use a library please do so. But, you just might find this interesting!
We did end up turning this experiment into a library, and iterated over it quite a bit, fixing bugs/etc. We also added string support. The library we ended up with is called pico-lambda, check it out (especially its source code, its tiny!), it also has a ton of unit tests and they run in browserstack across the major browsers too.
After it was all said and done, and we worked out edge cases and fixed bugs, added strings to the mix, we ended up with a slightly bigger “Box”
Object.getOwnPropertyNames(Array.prototype).reduce((lambda, method) => {lambda[method] = (~['concat', 'every', 'filter', 'find', 'findIndex', 'includes', 'join', 'map', 'reduce', 'reduceRight', 'slice', 'some'].indexOf(method))? (fn, ...params) => arr => arr[method](fn, ...params): (~['sort', 'copyWithin', 'fill'].indexOf(method))? (...params) => arr => [...arr][method](...params): (~['toLocaleString', 'indexOf', 'lastIndexOf'].indexOf(method))? (...params) => arr => arr[method](...params): (~['push', 'splice'].indexOf(method))? (...params) => arr => { var t = [...arr]; t[method](...params); return t; }: (~['toString', 'entries', 'keys'].indexOf(method))? arr => arr[method](): lambda[method];...full source here
Thanks for reading, if you like this please clap and follow me. Also leave a comment if you’d like to share your thoughts or have any questions!
❤