parsinghaskellfunctorapplicativehasql

Consuming Hasql statement outputs using something like a parser


I have an application which models a data domain using some deeply nested record structures. A contrived but analogous example would be something like:

Book
  - Genre
  - Author
    - Hometown
      - Country

I've found that when writing queries using Hasql (or Hasql-TH to be more precise), I end up with this enormous function which takes a huge tuple and constructs my record by effectively consuming this tuple tail-first and constructing these nested record types, before finally putting it all together in one big type (including transforming some of the raw values etc.). It ends up looking something like this:

bookDetailStatement :: Statement BookID (Maybe Book)
bookDetailStatement = dimap
  (\ (BookID a) -> a)    -- extract the actual ID from the container
  (fmap mkBook)          -- process the record if it exists
  [maybeStatement|
    select
      (some stuff)
    from books
    join genres on (...)
    join authors on (...)
    join towns on (...)
    join countries on (...)
    where books.id = $1 :: int4
    limit 1
  |]

mkBook (
  -- Book
  book_id, book_title, ...
  -- Genre
  genre_name, ...
  -- Author
  author_id, author_name, ...
  -- Town
  town_name, town_coords, ...
  -- Country
  country_name, ...
) = let {- some data processing -} in Book {..}

This has been a bit annoying to write and to maintain / refactor, and I was thinking about trying to remodel it using Control.Applicative. That got me thinking that this is essentially a type of parser (a bit like Megaparsec) where we are consuming an input stream and then want to compose parsing functions which take some "tokens" from that stream and return results wrapped in the Parsing Functor (which really should be a Monad I think). The only difference is that, since these results are nested, they also need to consume the outputs of previous parsers (although actually you can do this with Megaparsec too, and with Control.Applicative). This would allow smaller functions mkCountry, mkTown, mkAuthor, etc. which could be composed with <*> and <$>.

So, my question is basically twofold: (1) is this a reasonable (or even common) approach to real-world applications of this kind, or am I missing some sort of obvious optimisation which would allow this code to be more composable; (2) if I were to implement this, is a good route to adapt Megaparsec to the job (basically writing a tokeniser for the query result I think), or would it be simpler to write a data type to contain the query result and output value and then add the Monad and Applicative instance definition?


Solution

  • If I understand you correctly your question is about constructing the mkBook mapping function by composing from smaller pieces.

    What does that function do? It maps data from denormalised form (a tuple of all produced fields) to your domain-specific structure consisting of other structures. It is a very basic pure function, where you just move data around based on your domain logic. So the problem sounds like a domain problem. As such it is not general, but specific to the domain of your application and hence trying to abstract over it will likely result in neither a reusable abstraction or a simpler codebase.

    If you discover patterns inside such functions, those are likely to be domain-specific as well. I can advise nothing better than to just wrap them in other pure functions and to compose by simply calling them. No need for applicatives or monads.

    Concerning parsing libs and tokenisation I really don't see how it has anything to do with the discussed problem, but I may be missing your point. Also I don't recommend bringing lenses in to solve such a trivial problem, you'll likely end up with a more complicated and less maintainable solution.