Frameworks
Yesod

Pagination in Yesod - from naive to monads

Posted on Oct 10, 2016 by Alexej Bondarenko

There are some examples for Yesod apps out there. Mostly very complete but not very beginner friendly. One common use case (for me) is pagination. I was very surprised to see that this is covered nowhere at all. Just the simpliest "Building my own blog"-example is missing such useful administration features like "paginate through all blog posts".

Sure, there are two plugin solutions out there: yesod-pagination and yesod-paginator (YES, different things!). Both libraries try to cover pagination (but have only one basic, simple example and no documentation at all. Plus, I'm pretty sure nobody maintains this anymore).

Hence, I went all the way on my own (at least you learn much more this way) and integrate it into this blog. What do we need for pagination? We need a GET query parameter, let's call it "page". Furthermore we need the overall amount of valid (active, published) entries to calculate our offsets, nextPage and previousPage. To pull the query parameter inside the do notaion we can use lookupGetParam in our Handler like this:

getBlogR :: Handler Html
getBlogR = do

  pageParam <- lookupGetParam "page"
  defaultLayout $ do
    $(widgetFile "sometemplate")

This will give us a Maybe Text, a Just in case the parameter is present and a Nothing if the parameter is missing. Since we need an Int instead of Text we need to transform this into Int by using Text.Read.readMaybe:

parseInt :: Maybe Text -> Maybe Int
parseInt maybeText =
  case maybeText of
    Just text -> readMaybe $ unpack text
    Nothing -> Nothing

Great! We have a simple, type-safe way to determine the page parameter. readMaybe works on Strings, so we have to unpack it first. Furthermore, by defining the result type as Maybe Int we are able to tell Haskell what we expect by calling the polymorphic readMaybe function.

Let's get our overall count for all (visible) blog posts:

getBlogR :: Handler Html
getBlogR = do

  pageParam <- lookupGetParam "page"

  entriesCount <- runDB $ selectCount $ \entry -> do
                  E.where_  (entry ^. BlogPostPublishedAt E.<=. E.val now)

  defaultLayout $ do
    $(widgetFile "sometemplate")

The selectCount function is taken from StackOverflow. I don't understand why this is not built-in in Esqueleto. Any other query - following the examples on the Esqueleto page regarding E.countRows - just does not work (for me)!

With the overall count and an optional page parameter we can now go on and define our previousPage and nextPage functions:

previousPage :: Maybe Int -> Maybe Int
previousPage maybePage =
  case maybePage of
    Just value ->
      -- calculating previous page depending on current page
      if value > 0 then Just $ value - 1 else Nothing
    Nothing -> 
      -- if we have no page param, we can not have a previous page
      Nothing 
nextPage :: Maybe Int -> Int -> Int -> Maybe Int
nextPage maybePage entries pageSize =
  case maybePage of
    Just currentPage ->
      -- check if we have a next page
      if (currentPage + 1) * pageSize > entries then Nothing else Just $ currentPage + 1
    Nothing ->
      if entries > pageSize then Just 1 else Nothing

Our nextPage function takes two additional parameters to calculate the next page because we don't want to show a next-page link if we don't have any further entries.

With the definition of both functions we can now start to use our pagination inside the handler. Please pay attention that we are working only with the Maybe monad here and can not use it directly in the do block of the Handler. Instead we will use let:

getBlogR :: Handler Html
getBlogR = do

  pageParam <- lookupGetParam "page"
  let maybePage = parseInt pageParam

  entriesCount <- runDB $ selectCount $ \entry -> do
                  E.where_  (entry ^. BlogPostPublishedAt E.<=. E.val now)

  let next = nextPage maybePage entries 15
       previous = previousPage maybePage

  defaultLayout $ do
    $(widgetFile "sometemplate")

We can now use maybePage for offset calculation, next to display a next link and previous to display a previous link. This is - what I call - a naive (coming from imperative world) implementation. If you have a closer look at all function, we made use of the switch/case notation a lot. This is often an indication to use monads. Furthermore we have to stick to our let definitions inside the do block of our handler function.

How can we improve this code ? By having a look at the type definition of our getBlogR function you can see that the do block locks us down in the Handler monad. Hence, we need to lift our computations of parseInt, nextPage and previousPage into it. Here, the liftM function comes in (a -> m a). Let's have a look if we can refactor the pageParam part. Wouldn't it be nice to just say "page <- getCurrentPage" so that page is an Int we can continue to work with?

getCurrentPage :: Yesod m => HandlerT m IO Int
getCurrentPage =
  liftM (fromMaybe 0 . getIntParam) $ lookupGetParam "page"

getIntParam :: Maybe Text -> Maybe Int
getIntParam maybeText = do
  text <- maybeText
  parseInt text

parseInt :: Text -> Maybe Int
parseInt text = do
  readMaybe $ unpack text

Whoa, wait a minute! What happens here? First of all, we defined the monadic result type so we can use our function inside the do block of our Handler. As you can see, we still use lookupGetParam "page" which will give us a Maybe Text. We apply getIntParam which is our parseInt function, using do notation instead of . On the result (Maybe Int) we apply fromMaybe with a default value 0 if the outcome is Nothing. This will help us to calculate the offset by assuming the default page is always 0. And as the last step we lift our result (Int) to our monad. Let's do this with nextPage and previousPage as well:

previousPage :: Yesod m => HandlerT m IO (Maybe Int)
previousPage =
  liftM (calculatePreviousPage . fromMaybe 0 . getIntParam) $ lookupGetParam "page"



nextPage :: Yesod m => Int -> Int -> HandlerT m IO (Maybe Int)
nextPage entries pageSize =
  liftM (calculate . fromMaybe 0 . getIntParam) $ lookupGetParam "page"
  where
    calculate = calculateNextPage entries pageSize



calculatePreviousPage :: Int -> Maybe Int
calculatePreviousPage currentPage =
  if currentPage <= 0 then Nothing else Just $ currentPage - 1



calculateNextPage :: Int -> Int -> Int -> Maybe Int
calculateNextPage entries pageSize currentPage =
  if (currentPage + 1) * pageSize > entries then Nothing else Just $ currentPage + 1

Looks much nicer as having to write the switch/case over and over again. Did you notice how we stick functions together to implement our nextPage and previousPage examples? Now, there is a small little and nasty bug inside this code. In our case a page can also be -1,-2,-3,-4,... Let's manually fix this by adding a new function:

ensurePositive :: Int -> Int
ensurePositive value = if value < 0 then 0 else value

Easy, right? Now just stick this one to our getCurrentPage method:

getCurrentPage :: Yesod m => HandlerT m IO Int
getCurrentPage =
  liftM (ensurePositive . fromMaybe 0 . getIntParam) $ lookupGetParam "page"

We don't need to apply ensurePositive to nextPage and previousPage because the constrains in calculateNextPage and calculatePreviousPage already take care of it. As the last step, let's refactor our Handler:

getBlogR :: Handler Html
getBlogR = do

  page <- getCurrentPage

  entriesCount <- runDB $ selectCount $ \entry -> do
                  E.where_  (entry ^. BlogPostPublishedAt E.<=. E.val now)

  next <- nextPage entries 15
  previous <- previousPage

  defaultLayout $ do
    $(widgetFile "sometemplate")

You can now use all three (page :: Int, next :: Maybe Int and previous :: Maybe Int) in your template or inside the offset calculation of your query.

If you have any comments, suggestions or questions please use the section below.