Functional programming isn't limited to reduce
, filter
, and map
; it's about functions. This means we don't have to rely on perverse knowledge like Array.from ({ length: x })
where an object with a length
property can be treated like an array. This kind of behavior is bewildering for beginners and mental overhead for anyone else. It think you'll enjoy writing programs that encode your intentions more clearly.
reduce
starts with 1 or more values and reduces to (usually) a single value. In this case, you actually want the reverse of a reduce
(or fold
), here called unfold
. The difference is we start with a single value, and expand or unfold it into (usually) multiple values.
We start with a simplified example, alphabet
. We begin unfolding with an initial value of 97
, the char code for the letter a
. We stop unfolding when the char code exceeds 122
, the char code for the letter z
.
const unfold = (f, initState) =>
f ( (value, nextState) => [ value, ...unfold (f, nextState) ]
, () => []
, initState
)
const alphabet = () =>
unfold
( (next, done, char) =>
char > 122
? done ()
: next ( String.fromCharCode (char) // value to add to output
, char + 1 // next state
)
, 97 // initial state
)
console.log (alphabet ())
// [ a, b, c, ..., x, y, z ]
Above, we use a single integer for our state, but other unfolds may require a more complex representation. Below, we show the classic Fibonacci sequence by unfolding a compound initial state of [ n, a, b ]
where n
is a decrementing counter, and a
and b
are numbers used to compute the sequence's terms. This demonstrates unfold
can be used with any seed state, even arrays or objects.
const fib = (n = 0) =>
unfold
( (next, done, [ n, a, b ]) =>
n < 0
? done ()
: next ( a // value to add to output
, [ n - 1, b, a + b ] // next state
)
, [ n, 0, 1 ] // initial state
)
console.log (fib (20))
// [ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765 ]
Now we have the confidence to write pagination
. Again, our initial state is compound data [ page, count ]
as we need to keep track of the page
to add, and how many pages (count
) we've already added.
Another advantage to this approach is that you can easily parameterize things like 10
or -5
or +1
and there's a sensible, semantic structure to place them in.
const unfold = (f, initState) =>
f ( (value, nextState) => [ value, ...unfold (f, nextState) ]
, () => []
, initState
)
const pagination = (totalPages, currentPage = 1) =>
unfold
( (next, done, [ page, count ]) =>
page > totalPages
? done ()
: count > 10
? done ()
: next (page, [ page + 1, count + 1 ])
, [ Math.max (1, currentPage - 5), 0 ]
)
console.log (pagination (40, 1))
// [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ]
console.log (pagination (40, 14))
// [ 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 ]
console.log (pagination (40, 38))
// [ 33, 34, 35, 36, 37, 38, 39, 40 ]
console.log (pagination (40, 40))
// [ 35, 36, 37, 38, 39, 40 ]
Above, there are two conditions which result in a call to done ()
. We can collapse these using ||
and the code reads a little nicer
const pagination = (totalPages, currentPage = 1) =>
unfold
( (next, done, [ page, count ]) =>
page > totalPages || count > 10
? done ()
: next (page, [ page + 1, count + 1 ])
, [ Math.max (1, currentPage - 5), 0 ]
)