11

How could I write the following code more functionally using ES6, without any 3rd party libraries?

// sample pager array
// * output up to 11 pages
// * the current page in the middle, if page > 5
// * don't include pager < 1 or pager > lastPage
// * Expected output using example:
//     [9,10,11,12,13,14,15,16,17,18,19]

const page = 14 // by example
const lastPage = 40 // by example
const pagerPages = page => {
  let newArray = []
  for (let i = page - 5; i <= page + 5; i++) {
    i >= 1 && i <= lastPage ? newArray.push(i) : null
  }
  return newArray
}

I would like to avoid Array.push, and possibly the for loop, but I'm not sure how I would achieve it in this situation.

3 Answers3

7

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 ]
    )
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • 1
    Two others deleted their answers using `from`, `fill`, `reduce`, `map`, and `filter` – Mulan Apr 30 '18 at 23:48
  • `we don't have to rely on perverse knowledge` ... lol, i guess thats still more understandable than your `unfold` callbacks. But nevertheless, good job :) – Jonas Wilms May 01 '18 at 09:36
  • @JonasW. Sure, "callbacks" are weird if you are not used to higher order functions and the notion of functions as normal data. But it is a well-known property of almost all modern languages. `Array.from ({ length: x })` on the other hand is a Javascript quirk (caused by duck typing), deeply buried in the guts of the language. –  May 01 '18 at 10:19
  • "Perverse" is maybe a little...harsh? Every language has weird corner cases. Suggest replacing with esoteric? language-specific? build an array with this One Weird Trick? – Jared Smith May 01 '18 at 12:00
  • @JonasW. the bulk of my answer was targeted toward answers deleted by their authors. I do consider it perverse to exploit a language's behaviours in this way. Despite the fact the output can be made correct, the program is unreadable (even admitted by you) because it was conceived with an exploit in mind. Our program should accept two numbers and build an array, but answers using `from` and `fill` *start* with an array, (or perverse *"array-like"*) and work backward from there. – Mulan May 01 '18 at 14:56
  • @JaredSmith I chose the word specifically to make people think twice about depending on such a behavior. Just because it works doesn't mean you should use it. I'm looking at you `+x`... – Mulan May 01 '18 at 15:14
  • @JonasW. I tweaked the identifiers in the `unfold` definition a bit. If we zoom out, we see a binary function that prepends a value on the recursive result, and nullary function that returns the empty result – we call the user-supplied lambda `f` with these two functions along with the initial state. The user can now drive the recursion using the signals provided to the lambda. – Mulan May 01 '18 at 15:20
  • @user633183 or for perverse bending of the language you could just `Array.apply(null, Array(n)).map(....` – Jared Smith May 01 '18 at 16:33
6
  const pageRange = (lastPage, page) => ((start, end) => Array.from({length: end - start + 1}, (_,i) => i + start))(Math.max(1, page - 5), Math.min(lastPage, page + 5));
 const newArray = pageRange(40, 14);

This is a purely functional approach. It uses Math.max/min to achieve the boundaries and then uses an IIFE to pass these boundaries to Array.from which will create an array of end - start elements and every of these elements will be the position in the array increased by the startvalue.


PS: IMO your code is actually much more concise (except from that unneccessary ternary) and far more readable than mys, just saying...

Jonas Wilms
  • 132,000
  • 20
  • 149
  • 151
  • Wups, your program is broken Jonas :( It produces `[9,10,...14,...17,18]` (missing `19`). It produces `[1,2,3,4,5]` (missing `[6,7,...11]`) for `pageRange(40, 1)`. It produces `[35,36,...39]` (missing `40`) for `pageRange(40, 40)`. – Mulan Apr 30 '18 at 23:45
  • @user633183 right, the upper boundary needs to be included. However `1 -> [1,2,3,4,5]` seems to be the expected behaviour, the OPs code will produce that too – Jonas Wilms May 01 '18 at 09:32
2

There are many ways to create an array functionally but creating an array depending on some correlated items like a math series is mostly done by unfolding. You may consider unfolding like the inverse of reducing. Your case do not necessarily require unfolding but just for the sake of proper functional programming lets see how it can be done.

JS do not have a native unfolding function but we may simply implement it. First of all what does unfold function look like..?

Array.unfold = function(p,f,t,v){
  var res = [],
   runner = d =>  p(d,res.length,res) ? [] : (res.push(f(d)),runner(t(d)), res);
  return runner(v);
};

As seen it takes 4 arguments.

  1. p: This is a callback function just like we have in reduce. It gets invoked with the current seed element e to be processed before insertion, it's index i to be inserted and the currently available array a like p(e,i,a). When it returns a true, the unfolding operation concludes and returns the created array.
  2. f: Is the function that we will apply for each item to be constructed. It takes a single argument which is the current iterating value. You may consider iterating value like the index value but we have control over how to iterate it.
  3. t: Is the function that we will apply the iterating value and get the next iterating value. For index like iterations this should be x => x+1.
  4. v: Is the glorious initial value.

So far so good. How are we going to use unfold to achieve this job. First of all lets find our initial value by a function which takes page as an argument.

var v = (pg => pg - 5 > 0 ? pg - 5 : 1)(page)

How about the p function which decides where to stop?

var p = (_,i) => i > 10

We will increase pages one by one but if we have a value greater than lastpage we need to feed null values instead. So f may look like

var f = (lp => v => v > lp ? null : v)(lastpage)

and finally t is the function how we increase the iterating value. It's x => x + 1.

Array.unfold = function(p,f,t,v){
  var res = [],
   runner = d =>  p(d,res.length,res) ? [] : (res.push(f(d)),runner(t(d)), res);
  return runner(v);
};

var v = pg => pg - 5 > 0 ? pg - 5 : 1,
    p = (_,i) => i > 10,
    f = lp => v => v > lp ? null : v,
    t = x => x + 1,
    a = Array.unfold(p,f(40),t,v(14));

console.log(a);
Redu
  • 25,060
  • 6
  • 56
  • 76