Pagination

by Roman Zolotarev

Recently I was working on a pagination for a web app. This simple problem can be a good case to compare JavaScript and Elm.

Here is the use case:

We have a current page address, e.g. index, and a list of all page addresses: index, vanilla, ramda, elm.

There are two links: Previous and Next. By clicking on those links you go to the previous or the next page accordingly.

When you are on the first page, Previous button is disabled. When on the last, Next is disabled.


              current page
                   |
                   |
 [ Previous ]    index    vanilla    ramda    elm    [ Next ]
      |
      |
disabled link

Let’s write two functions. One to prepare a data structure and another to generate HTML code based on it.

paginate() function

This function takes a current page and a list of pages. It returns a tuple of previous, next. Both elements of the tuple can be empty.

html() function

This one takes a result of paginate() and returns HTML string for two links. It handles cases when a current page is not found, or either of links is missing.

// List of pages
pages = ['index', 'vanilla', 'ramda', 'elm']

// Previous should be disabled
paginate('index', pages);
// => [undefined, 'vanilla']

html(paginate('index', pages));
// => 'No previous<a href="vanilla">Next</a>'

// Both links are available
html(paginate( 'vanilla', pages));
// => '<a href="index">Previous</a><a href="ramda">Next</a>'

// Non-existent page
html(paginate( 'pageX', pages));
// => 'Current not found'

For the beginning, let’s implement this in vanilla JavaScript.

Vanilla JavaScript

Let’s start with plain JavaScript implementation, to be specific, with ECMAScript 5. Then we can review what we can improve.

How can we implement paginate() in JavaScript? Get an index of current page in the array of pages. If current page is found, then get its neighbors and return an array [previous, next], otherwise return nothing.

With html() it is pretty straightforward. Get that array and render it to a string.

Naïve implementation in ECMAScript 5.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var pages = ['index', 'vanilla', 'ramda', 'elm'];
var current = 'vanilla';

var paginate = function(current, pages) {

  // Get index of current page
  // in array of pages
  var i = pages.indexOf(current);

  // If current page is found
  // (index is not -1)
  return i !== -1
    ? ([ pages[i - 1], // Previous page
         pages[i + 1]  // Next page
      ])
    // else return undefined
    : undefined;
}

var html = function(pagination) {
  if (pagination === undefined) return 'Current not found';

  var previous =
    pagination[0] === undefined
      ? 'No previous'
      : ('<a href="' + pagination[0] + '">Previous</a>');

  var next =
    pagination[1] === undefined
      ? 'No next'
      : ('<a href="' + pagination[1] + '">Next</a>');

  return (previous + next);
}

Now we can test our functions.

pages = ['index', 'vanilla', 'ramda', 'elm']
// => '["index", "vanilla", "ramda", "elm"]'

current = 'vanilla'
// => "vanilla"

paginate(current, pages)
// => ["index", "ramda"]

html(paginate(current, pages))
// => "<a href=\"index\">Previous</a><a href=\"ramda\">Next</a>"

How can we improve it?

First, those undefined values looks suspicious. Just take a look at the edge case, when previous is undefined:

paginate('index', pages)
// => [undefined, "vanilla"]

We need some safer data types.

Second, those if and else statements. It is possible to forget and accidentally miss one of those possible cases.

We need a better way to branch our code.

Built with Ramda

If you have not tried functional programming style before, it may look unusual, but it totally worth learning. Because it is consistently simple, backed by math, and fun to learn.

Before we dive in into implementation, get familiar with Ramda.

Ramda

Ramda helps us to write code in a purer functional style. Ramda is a small library, has no dependencies, and works in all browsers as well as Node.

var numbers = [10, 20, 30, 40];
var inc = function (x) { return (x + 1); }

Compare native map

numbers.map(inc)
// => [11, 21, 31, 41]

… with Ramda’s:

R.map(inc, numbers)
// => [11, 21, 31, 41]

It looks not that different at the beginning. It is only at the beginning.

How map() works? It takes a function and a functor (e.g. array, string, or object), applies the function to each of the functor’s values, and returns the functor of the same shape.

R.map(inc, {a: 100, b: 200})
// => {"a": 101, "b": 201}

R.map(inc, {a: 'a', b: 'b'})
// => {"a": "a1", "b": "b1"}

R.map(inc, 'abc')
// => ["a1", "b1", "c1"]

What happens when you pass fewer parameters than function expects? Usually JavaScript functions are not happy.

numbers.map()
// => TypeError: Array.prototype.map callback must be a function

Now look at Ramda function.

R.map()
// => function n(r,e){switch(arguments.length){case 0:return n;case 1:return b(r)?n:L(function(n){return t(r,n)});default:return b(r)&&b(e)?n:b(r)?L(function(n){return t(n,e)}):b(e)?L(function(n){return t(r,n)}):t(r,e)}}

Right, it returns a function. If there were no parameters passed, it returns itself. Just double checking.

R.map
// => function n(r,e){switch(arguments.length){case 0:return n;case 1:return b(r)?n:L(function(n){return t(r,n)});default:return b(r)&&b(e)?n:b(r)?L(function(n){return t(n,e)}):b(e)?L(function(n){return t(r,n)}):t(r,e)}}

What if you pass one parameter instead of two?

incAll = R.map(inc)
// => function n(r){return 0===arguments.length||b(r)?n:t.apply(this,arguments)}

It returns a partially applied function.

All Ramda functions are automatically curried and work this way. A curried function can take only a subset of its parameters and returns a new function that takes the remaining parameters. If you call a curried function with all of its parameters, it just calls the function and returns a value.

So all of the following expressions return the same result.

incAll(numbers)
// => [11, 21, 31, 41]

R.map(inc, numbers)
// => [11, 21, 31, 41]

R.map(inc)(numbers)
// => [11, 21, 31, 41]

R.map()(inc)(numbers)
// => [11, 21, 31, 41]

Please note, in most Ramda functions the data is supplied as the last parameter to make it convenient for currying.

By the way, Ramda has R.inc() and R.dec() already.

R.map(R.dec)(numbers)
// => [9, 19, 29, 39]

Probably the simplest Ramda function is R.identity(). It does nothing but returns the parameter supplied to it. Good as a placeholder function. We will use it in the example.

R.map(R.identity)(numbers)
// => [10, 20, 30, 40]

These two function names speak for themselves. R.head() returns the first element of the array.

R.head(numbers)
// => 10

R.last() returns the last element of the array.

R.last(numbers)
// => 40

The last function you need to learn for now: R.concat().

R.concat(numbers, 50)
// => [10, 20, 30, 40, 50]

I am glad you get that far. Finally, we get to the point where we can get rid of undefined from our program. There is a library for this. :)

Sanctuary

Sanctuary is a functional programming library. It depends on and works nicely with Ramda.

Let’s compare two indexOf functions, when the element is not found in the array:

R.indexOf(42, numbers)
// => -1

-1? Okay. Kind of weird, but familiar. Now look at Sanctuary’s version:

S.indexOf(42, numbers)
// => Nothing()

It returns a value of Maybe type. Maybe is useful for composing functions that might not return a value.

One more example of a function returning Maybe.

S.at() takes an index and a list and returns Just the element of the list at the index, if the index is within the list’s bounds…

S.at(1, numbers)
// => Just(20)

… and returns Nothing otherwise.

S.at(100, numbers)
// => Nothing()

Compare it with the native JavaScript alternative:

numbers[100]
// => undefined

Okay, sounds like we can get rid of undefined. But how to handle Maybe values? To apply functions to Maybe values you can use old good map:

R.map(R.inc, S.Just(3))
// => Just(4)

R.map(R.inc, S.Nothing())
// => Nothing()

But what about branching code without using conditionals? In our case, we have two branches. Left and right.

pagination[2] === undefined
  ? 'No next'                                     // Left
  : ('<a href="' + pagination[2] + '">Next</a>'); // Right

To handle it, we can convert Maybe to Either. What is Either? The Either type represents values with two possibilities. Left and Right.

S.maybeToEither('No next', S.Just(3))
// => Right(3)

S.maybeToEither('No next', S.Nothing())
// => Left("No next")

Now we can apply two different functions to Left and Right.

When the third parameter is Left, then R.identity() is applied to 'No next'. Let’s hard-code the third parameter to test S.either.

S.either(R.identity, R.toString, S.Left('No next'))
// => "No next"

When the third parameter is Right, then R.toString() is applied to 3. The third parameter is hard-coded again.

S.either(R.identity, R.toString, S.Right(3))
// => "3"

Congratulations! Now you are ready to read the refactored solution. ;)

Implementation with Ramda and Sanctuary

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// var R = require('ramda');
// var S = require('sanctuary');

var pages = ['index', 'vanilla', 'ramda', 'elm'];
var current = 'ramda';

var paginate = function(current, pages) {

  return R.map(
    function(index) {
      return [
        S.at(R.dec(index), pages),
        S.at(R.inc(index), pages)
      ]
    },
    // Gets index of the current page
    S.indexOf(current, pages)
  )
}

var html = function(pagination) {

  var previous =
    function(url) {
      return ('<a href="' + url + '">Previous</a>');
    }

  var next =
    function(url) {
      return ('<a href="' + url + '">Next</a>');
    }

  var buttons = function(x) {
    return R.concat(
      S.either(
        R.identity,
        previous,
        S.maybeToEither('No previous', R.head(x))
      ),
      S.either(
        R.identity,
        next,
        S.maybeToEither('No next', R.last(x))
      )
    )
  }

  return S.either(
    R.identity,
    buttons,
    S.maybeToEither('Current not found', pagination)
  )
}
pages = ['index', 'vanilla', 'ramda', 'elm']
// => ["index", "vanilla", "ramda", "elm"]

current = 'ramda'
// => "ramda"

paginate('ramda', pages)
// => Just([Just("vanilla"), Just("elm")])

html(paginate('ramda', pages))
// => "<a href=\"vanilla\">Previous</a><a href=\"elm\">Next</a>"

What can we improve?

Syntax. It is getting pretty lengthy and noisy. ECMAScript 2015 solves this partially.

const paginate = (current, pages) => R.map(
  (i) => R.map(
    S.at(R.dec(i), pages),
    S.at(R.inc(i), pages)
  ), S.indexOf(current, pages)
)

By the way, here is our vanilla version in ECMAScript 2015.

const paginate = (current, pages) => {
  const i = pages.indexOf(current);
  return i !== -1
    ? [pages[i - 1], pages[i + 1]]
    : undefined;
}

Another thing about our solution is that html() returns a string. It would be good to have DOM elements instead. To manipulate DOM we can use one of Virtual DOM libraries or frameworks, e.g. React, Angular, Ember, Vue.

But if we are looking for lighter and purely functional solution, then Elm can be a better option.

Built with Elm

I have been playing with Elm for several months and find it fascinating. Helpful error messages. If it compiles, it works. No runtime errors. Very good performance. Clean syntax. Simple refactoring, and so on.

How to write in Elm?

Create a file, e.g. pagination.elm. If you are using any modules, you have to import them explicitly.

For implementation in Elm I am using List type for the list of pages. To get value by index, we can use getAt() and to get index by value — elmeIndex(). Both functions are from List.Extra module. Let’s import it.

import List.Extra

List.Extra.getAt 3 pages
-- Just "elm"

We can use the exposing keyword to import particular functions of the module, and then call them without a qualifier.

import List.Extra exposing (getAt, elemIndex)

getAt 3 pages
-- Just "elm"

elemIndex "elm" pages
-- Just 3

As you can see there are no brackets needed in these function calls. Just function name and list of its parameters.

To define a function you just type in a function name, then a list of its parameters, then = sign and finally its definition.

inc x = x + 1

inc 1
-- 2

You can define local functions inside a function with let ... in keywords.

current = "ramda"

isCurrent page =
    let
        current = "elm"
    in
        page == current

isCurrent "elm"
-- True

To handle cases, for parameters, you can use case ... of keywords, which is useful for pattern matching. The important difference with Elm: its compiler won’t let you forget not covered cases and will make sure all branches return values of the same shape.

get index pages =
    case index of
        Just i ->
            "Index is " ++ i

        Nothing ->
            "No index"

You probably noticed that for summing numbers in Elm we use +, but to concatenate strings we need ++.

Our function paginate() returns a tuple. It is a data type in Elm. Here is the syntax.

pagination = (Just "elm", Nothing)

Oh, and Maybe type is in Elm core library, you don’t need to import it.

HTML elements are functions of Html module. So to create a link — HTML element <A> — you need to call function Html.a with two parameters: list of attributes and list of children.

Html.a [ Html.Attributes.href "ramda" ] [ Html.text "Previous" ]

As you can guess text is a function from Html module, and href is a function from Html.Attributes module.

That is it. You know enough of Elm to read the code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import Html exposing (div, text, a)
import Html.Attributes exposing (href)
import List.Extra exposing (getAt, elemIndex)


pages =
    [ "index", "vanilla", "ramda", "elm" ]


current =
    "elm"


paginate current pages =
    case (elemIndex current pages) of
        Just i ->
            ( (getAt (i - 1) pages), (getAt (i + 1) pages) )

        _ ->
            ( Nothing, Nothing )


html pagination =
    let
        next page =
            case page of
                Just url ->
                    a [ href url ] [ text "Next" ]

                Nothing ->
                    text "No next"

        previous page =
            case page of
                Just url ->
                    a [ href url ] [ text "Previous" ]

                Nothing ->
                    text "No previous"
    in
        case pagination of
            ( Nothing, Nothing ) ->
                text "Current not found"

            ( p, n ) ->
                div [] [ previous p, next n ]


main =
    html (paginate current pages)

Epilog

You may ask, what the point of adding lines of code, introducing new libraries, and unusual data types? Even a new language. Doesn’t it complicate all thing?

First, type safety helps you to avoid runtime errors and catch mistakes earlier. Second, pure functions are much simpler to understand, to maintain, and to refactor. Third, composability and point-free style in most cases make your code much cleaner.

If you are building a small web app and file size is critical, then nothing can beat vanilla JavaScript, of course. However, when your app grows, Ramda or Elm can save you from mistakes and make your developer experience much better.

You still better know JavaScript equality quirks, how to manipulate DOM directly, how to manage scope, avoid mutable state, how to handle null type, etc.

If JavaScript is so weird, why should we learn it? Because 3 billion people use web browsers today, and JavaScript is the only language web browsers support today. JavaScript is getting better, but it can’t be changed dramatically anytime soon.

Bonus track

Thank you for reading that far. Here is another implementation with Elm core library only. Copy-paste it to Elm REPL and play with it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import Html
import List

pages = [ "index", "vanilla", "ramda", "elm" ]
current = "elm"

paginate current pages =
    let
        cs = pages |> List.map Just
        ns = List.drop 1 cs ++ [ Nothing ]
        ps = Nothing :: List.take (List.length cs - 1) cs
        isCurrent (p, c, n) =
            case c == Just current of
                True -> Just (p, n)
                False -> Nothing
    in
        List.map3 (\p c n -> ( p, c, n )) ps cs ns
          |> List.filterMap isCurrent

main =
    paginate current pages
      |> toString
      |> Html.text
      -- Just (Just "ramda", Just "elm", Nothing)

Further reading

Illustration: Pagination