Introduction
In this post, I will explain how it is possible to use fold
(in Haskell) or reduce
(in JavaScript) to rewrite some common array functions to get a basic understanding of how they work and how much you can do with it.
So, if you want to learn how fold
/reduce
work and what a powerful function it is, this post is for you.
How does fold/reduce work?
fold
/reduce
iterates over a list and operates on the current element and the already processed ones.
I must admit that this sounds a bit confusing, but I hope you will understand what that means while reading this article.
There is a slight difference in the Haskell fold
and the JavaScript reduce
, the Haskell one is curried by default but that shouldn’t bother us.
Let us look at the function definition first:
foldl :: Foldable t => (b -> a -> b) -> b -> t a -> b
This look weird, an annotated version would look like this:
( first param ) -> ( second param ) (third param)
(Accumulator -> CurrentElement -> Result) (NeutralElement) -> (Foldable) -> Result
foldl :: Foldable t => ( b -> a -> b ) -> b -> t a -> b
Foldable
is a so-called Type class and says that the type t
must implement the Foldable
interface, as long as it does nothing matters.
The first argument is a function which takes two arguments, the so-called accumulator which contains the already calculated result until this stage and the current element of the Foldable
which is processed now.
This function returns a result which is the accumulator in the next run.
The second argument is the neutral element which is the accumulator of the first iteration of this function. You can think of it as the default value or the starting value.
The third argument is the Foldable
over which the function is iterating, this can be a List/Array for example.
So, fold
/reduce
needs a function which gets two arguments the current element of the iteration and the result of the already processed iterations, a neutral element and a list and returns something the same type as the neutral element.
fold
/reduce
iterates over a list(Foldable
) and operates on the current element and the already processed ones.
Does this sound more clear now? Don’t worry if not, you will get it when looking at the examples.
Theoretically, you can do everything on a Foldable
you want. You can calculate a single result out of it (like a sum
function or a length
function), calculate a new value for each element (like map
) or transform the values into a completely new structure.
Still confused? Then go and read on how we implement some functions.
Length
Length is a function that gets an array and returns the amount of the elements inside an array. So it counts the array elements.
Haskell implementation:
length' :: [a] -> Int
length' = foldl (\acc _ -> acc + 1) 0
length' [1..5] -- 5
JavaScript implementation:
const length = arr => arr.reduce(acc => acc + 1, 0)
length([1, 2, 3, 4, 5]); // 5
We iterate over the array and add one for each element to the accumulator, which is zero as the default. As we don’t need the actual value of the current element we leave this argument blank.
Sum/Max/Min
Sum
Sum is a function that gets an array and returns the sum of the elements of that array.
Haskell implementation:
sum' :: [Int] -> Int
sum' = foldl (\acc curr -> acc + curr) 0
sum' [1..5] -- 15
JavaScript implementation:
const sum = arr => arr.reduce((acc, curr) => acc + curr, 0);
sum([1, 2, 3, 4, 5]); // 15
We iterate over the array and add the value of the current value to the accumulator, which is zero as the default.
Max
Max is a function that gets an array and returns the maximum of that array.
Haskell implementation:
max' :: [Int] -> Int
max' (x:xs) = foldl (\acc curr -> if curr > acc then curr else acc) x xs
max' [1..5] -- 5
JavaScript implementation:
const max = arr => arr.reduce((acc, curr) => (curr > acc ? curr : acc), arr[0]);
max([1, 2, 3, 4, 5]); // 5
We iterate over the array and look if the current element is greater than the accumulator, if so we use the current element otherwise we use the accumulator and the neutral element is the first element of the array. This returns the maximum of an array.
Min
Min is a function that gets an array and returns the minimum of that array.
The implementation is similar to the max
-function but with the opposite comparison.
Haskell implementation:
min' :: [Int] -> Int
min' (x:xs) = foldl (\acc curr -> if curr < acc then curr else acc) x xs
min' [1..5] -- 1
JavaScript implementation:
const min = arr => arr.reduce((acc, curr) => (curr < acc ? curr : acc), arr[0]);
min([1, 2, 3, 4, 5]); // 1
Elem(Includes)/Delete
Elem(Includes)
Elem, in JavaScript it is called includes, is a function that gets an element and an array and returns whether the element is an element of that array or not.
Haskell implementation:
elem' :: Eq a => a -> [a] -> Bool
elem' item = foldl (\acc curr -> if item == curr then True else acc ) False
elem' 3 [1..5] -- True
elem' 8 [1..5] -- False
JavaScript implementation:
const includes = item => arr =>
arr.reduce((acc, curr) => (item === curr ? true : acc), false);
includes(3)([1, 2, 3, 4, 5]); // true
includes(8)([1, 2, 3, 4, 5]); // false
We iterate over the array and check if the current element is the same as the element we are searching for if it is we return true otherwise we return false and on the first run the accumulator is set to false. So if the element is inside the array we set the accumulator to true and it has no possibility to be set to false again.
Delete
Delete is a function that gets an element and an array and returns the array without every occurrence of that element.
Haskell implementation:
delete' :: Eq a => a -> [a] -> [a]
delete' item = foldl (\acc curr -> if item == curr then acc else acc ++ [curr] ) []
delete' 3 [1..5] -- [1,2,4,5]
JavaScript implementation:
// delete is a keyword in JavaScript
const myDelete = item => arr =>
arr.reduce((acc, curr) => (item === curr ? acc : acc.concat(curr)), []);
myDelete(3)([1, 2, 3, 4, 5]); // [ 1, 2, 4, 5 ]
We iterate over the list and check if the current element is the same as the one we want to delete from the list, if so we return the accumulator otherwise we concatenate the accumulator with the current element and return the result. The neutral element is an empty array.
Head/Last/Tail/Reverse
Head
Head is a function that gets an array and returns the first element of that array.
Haskell implementation:
head' :: [a] -> a
head' (x:xs) = foldl (\acc _ -> acc) x xs
head' [1..5] -- 1
JavaScript implementation:
const head = arr => arr.reduce(acc => acc, arr[0]);
head([1, 2, 3, 4, 5]); // 1
We set the neutral element to the first element of the array and in the processing function, we return only the accumulator, so the result can’t be anything but the first element.
Last
Last is a function that gets an array and returns the last element of that array.
Haskell implementation:
last' :: [a] -> a
last' (x:xs) = foldl (\_ curr -> curr) x xs
last' [1..5] -- 5
JavaScript implementation:
const last = arr => arr.reduce((_, curr) => curr, arr[0]);
last([1, 2, 3, 4, 5]); // 5
We set the neutral element to the first element of the array and in the processing function, we always return the current element, so the result is the last element eventually.
Tail
Tail is a function that gets an array and returns all except the first element of that array.
Haskell implementation:
tail' :: [a] -> [a]
tail' (x:xs) = foldl (\acc curr -> acc ++ [curr]) [] xs
tail' [1..5] -- [2,3,4,5]
JavaScript implementation:
const tail = arr =>
arr.reduce((acc, curr, index) => (index > 0 ? acc.concat(curr) : acc), []);
tail([1, 2, 3, 4, 5]); // [ 2, 3, 4, 5 ]
This time the Haskell and the JavaScript implementation is slightly different.
In Haskell, we use everything but the first element as the processing list and concatenate every element onto the accumulator.
In JavaScript, we iterate over the whole list but use the index
argument which is coming from reduce to check if the current element is the first element (index 0) or not and concatenate the elements onto the accumulator.
The neutral element is an empty array.
Reverse
Reverse is a function that gets an array and returns the array in the opposite order.
Haskell implementation:
reverse' :: [a] -> [a]
reverse' = foldl (\acc curr -> curr : acc ) []
reverse' [1..5] -- [5,4,3,2,1]
JavaScript implementation:
const reverse = arr => arr.reduce((acc, curr) => [curr].concat(acc), []);
reverse([1, 2, 3, 4, 5]); // [ 5, 4, 3, 2, 1 ]
We iterate over the array and concatenate the accumulator onto the current element on each iteration.
The neutral element is an empty array.
All/Any
All
All is a function that gets a function (from the element of that list to bool) and an array and returns whether every element in that array matches the condition.
Haskell implementation:
all' :: (a -> Bool) -> [a] -> Bool
all' f = foldl (\acc curr -> f curr && acc) True
all' (>2) [1..5] -- False
all' (>0) [1..5] -- True
JavaScript implementation:
const all = fn => arr => arr.reduce((acc, curr) => fn(curr) && acc, true);
all(i => i > 2)([1, 2, 3, 4, 5]); // false
all(i => i > 0)([1, 2, 3, 4, 5]); // true
We get a function as an argument and we set the neutral element to True
.
We process every element with the function and do a logical and
with the accumulator.
Any
Any is a function that gets a function (from the element of that list to bool) and an array and returns whether any element in that array matches the condition.
Haskell implementation:
any' :: (a -> Bool) -> [a] -> Bool
any' f = foldl (\acc curr -> f curr || acc) False
any' (>2) [1..5] -- True
any' (>10) [1..5] -- False
JavaScript implementation:
const any = fn => arr => arr.reduce((acc, curr) => fn(curr) || acc, false);
any(i => i > 2)([1, 2, 3, 4, 5]); // true
any(i => i > 10)([1, 2, 3, 4, 5]); // false
Similar to all
but we set the neutral element to false and do a logical or
with the accumulator and the processed element.
Take
Take is a function that gets a positive integer and an array and returns an array with the first elements until the list is as big as the passed integer.
Haskell implementation:
take' :: Int -> [a] -> [a]
take' n = foldl (\acc curr -> if length acc == n then acc else acc ++ [curr]) []
take' 2 [1..5] -- [1,2]
JavaScript implementation:
const take = n => arr =>
arr.reduce((acc, curr) => (acc.length === n ? acc : acc.concat(curr)), []);
take(2)([1, 2, 3, 4, 5]); // [ 1, 2 ]
The neutral element is an empty list and we check if the accumulator is of the length of the number of elements we want to receive, if so we just return the accumulator otherwise we concatenate the accumulator with the current element and return that result.
Map
Map is a function that gets a function and an array and returns an array of the same size where every element was applied to that function.
Haskell implementation:
map' :: (a -> b) -> [a] -> [b]
map' f = foldl (\acc curr -> acc ++ [f curr]) []
map' (*2) [1..5] -- [2,4,6,8,10]
JavaScript implementation:
const map = fn => arr => arr.reduce((acc, curr) => acc.concat(fn(curr)), []);
map(i => i * 2)([1, 2, 3, 4, 5]); // [ 2, 4, 6, 8, 10 ]
We concatenate the accumulator with the result of the function we get as an argument and the current element and return that inside our processing function.
Filter
Filter is a function that gets a function (from the element of that list to bool) and an array and returns a new array with the elements of the first list matching these condition.
Haskell implementation:
filter' :: (a -> Bool) -> [a] -> [a]
filter' f xs = foldl (\acc curr -> if f curr then acc ++ [curr] else acc ) [] xs
filter' (<4) [1..5] -- [1,2,3]
JavaScript implementation:
const filter = fn => arr =>
arr.reduce((acc, curr) => (fn(curr) ? acc.concat(curr) : acc), []);
filter(i => i < 4)([1, 2, 3, 4, 5]); // [ 1, 2, 3 ]
In our processing function, we check if the current element applied to the passed function is True
, if so we concatenate the accumulator with the current element otherwise we only return the accumulator.
Closing
I hope you can see that fold
/reduce
is a very powerful tool and that you can do anything on arrays with it.
If we would reread the sentence from before I think it feels much clearer now:
fold
/reduce
iterates over a list(Foldable
) and operates on the current element and the already processed ones.
If you want to read more about it I would recommend these three links: