2017-03-20

Reduce everything

Today I decided to do a small excercise and rewrite as many Array.prototype methods with reduce() as possible. Why? No particular reason, but I'd compare it to kicking a bucket with water just to see how it spills and perhaps learning something from the observation.

In holistic view of universe, like in one of those Greek tragedies, all the elements will fall into the place. There is maybe no particular purpose in it, but there is beginning. A while back, for fun, I started learning Haskell. This is nothing new these days, as a lot of people is learning this language for fun. Not that it's bad, quite contrary. In tutorial I eventually settled with, the author was rewriting native/out-of-the-box functions (I'm still too new to Haskell to figure out if native is the word there). I didn't quite see why, apart from just being interesting introduction to language. But then another piece fell in. Eric Elliott did the same thing in his "Reduce (Composing Software)". It was to show the hidden power of the concept of reduce, and it worked. I found it a bit eye-opening, perhaps even mindblowing, because the day before I wrote in "Looper turned splitter" that reduce() is rarely used. Turns out in a bare form, yeah, but that's because there is a range variety of specialised variants.

Let's see how deep it goes.

Originals

This is exactly how it started. Two pieces of code. Six lines total. And yet.

(Bothe pieces of code taken from: medium.com/javascript-scene/reduce-composing-software-fe22f0c39a1d#.4chgx5fc0.)

map()

const map = (fn, arr) => arr.reduce((acc, item, index, arr) => {
    return acc.concat(fn(item, index, arr));
}, []);

filter()

const filter = (fn, arr) => arr.reduce((newArr, item) => {
    return fn(item) ? newArr.concat([item]) : newArr;
}, []);

Others

slice()

Rewriting this one I mentioned in a comment to Eric's article. Actually doing it now I saw it's much more complex operation than I initially thought, all thanks to possibility of minus values. And this is without checking typeof of arguments passed. Just bare logic.

const slice = (array, from, to) => {
    const startIndex = from < 0
        ? array.length + from
        : from;
    const endIndex = to
        ? (to < 0 ? index < array.length + to : index < to)
        : index < array.length;

    return array.reduce((accumulated, current, index) =>
        index >= startIndex && index < endIndex
            ? accumulated.concat(current)
            : accumulated, 
        [],
   );
};

Mind you, we need to calculate start and end indexes only once. (At first I was doing this in every iteration and that wasn't the smartest thing.)

indexOf(), lastIndexOf, and findIndex()

All three return index but under different conditions.

const indexOf = (haystack, needle, fromIndex = 0) => {
    const startIndex = fromIndex < 0 ?
        haystack.length + fromIndex
        : fromIndex;
    
    return haystack.reduce((answer, current, index) =>
       index >= startIndex && answer == -1 && current === needle
           ? index
           : answer,
       -1,
  );
};
const lastIndexOf = (haystack, needle, fromIndex = 0) => {
    const startIndex = fromIndex < 0
        ? haystack.length + fromIndex
        : fromIndex;

    return haystack.reduce((answer, current, index) =>
       index >= startIndex && current === needle
           ? index
           : answer,
        -1,
    );
}
const findIndex = (array, fn) =>
    array.reduce((foundIndex, current, index) =>
        foundIndex == -1 && fn(current)
            ? index
            : foundIndex,
        -1,
    );

includes(), some(), and every()

const includes = (array, element, fromIndex = 0) => {
    const startIndex = fromIndex < 0 ?
        array.length + fromIndex
        : fromIndex;
    
    return array.reduce((finalBool, current, index) =>
        index >= fromIndex && current === element ? true : false,
        false,
    );
};
const some = (array, fn) =>
    array.reduce((finalBool, current, index, originalArray) =>
        fn(current, index, originalArray) ? true : false,
        false,
    );
const every = (array, fn) =>
    array.reduce((finalBool, current, index, originalArray) =>
        !fn(current, index, originalArray) ? false : true,
        true,
    );

Functions some() and every() are kind of opposites to each other when it comes to their logic.

find()

This one turned out to be very interesting because as a control value I couldn't have used -1, or false, or null, or even undefined because any of this values is valid and user might be using just that particular one. ES6's Symbol came handy, as Symbol() creates unique value. Using inner scope, I made unfound known and accessible only inside.

const find = (array, fn) => {
    const unfound = Symbol();
    
    const foundItem = array.reduce((found, current) =>
        found === unfound && fn(current) ? current : unfound,
        unfound,
    );
    
    return foundItem === unfound ? undefined : foundItem;
 };

(I know there is this elaborate hack with Proxy to steal Symbol's value, but in normal cases it will work.)

reverse()

const reverse = array =>
    array.reduce((final, current) =>
        [current, ...final),
        [],
    );

This one is peculiar, though. All the previous functions don't mutate original arrays. This one does. Which I learned while writing this story. (People, write as many stories as possible for your own sake!) So, apart from doing the original job, this function passes my secret test of character. Neat.

(It's also oblivious to Symbol.isConcatSpreadable. I probably could have been more considerate.)

Conclusion

So what's the conclusion? In perfect case scenario, it shows the underlying reducibility of actions taken by many Array methods. As for me, it was nice way to consolidate my knowledge.