2
votes

Haxl is an amazing library, but one of the major pain points I find is caused by the fact that each sort of request to the data source requires its own constructor in the Request GADT. For example, taking the example from the tutorial:

data BlogRequest a where
  FetchPosts       :: BlogRequest [PostId]
  FetchPostContent :: PostId -> BlogRequest PostContent

Then each of these constructors are pattern matched on and processed separately in the match function of the DataSource instance. This style results in a lot of boilerplate for a non-trivial application. Take for example an application using a relational database, where each table has a primary key. There may be many hundreds of tables so I don't want to define a constructor for each table (let alone on all the possible joins across tables...). What I really want is something like:

data DBRequest a where
  RequestById :: PersistEntity a => Key a -> DBRequest (Maybe a)

I'm using persistent to create types from my tables, but that isn't a critical detail -- I just want to use a single constructor for multiple possible return types.

The problem comes when trying to write the fetch function. The usual procedure with Haxl is to pattern match on the constructor to separate out the various types of BlockedFetch requests, which in the above example would correspond to something like this:

        resVars :: [ResultVar (Maybe a)]
        args :: [Key a]
        (psArgs, psResVars) = unzip
            [(key, r) | BlockedFetch (RequestById key) r <- blockedFetches]

...then I would (somehow) group the arguments by their key type, and dispatch a SQL query for each group. But that approach won't because here may be requests for multiple PersistentEntity types (i.e. database tables), each of which is a different type a, so building the list is impossible. I've thought of using an existentially quantified type to get around this issue (something like SomeSing in the singletons library), but then I see no way to group the requests as required without pattern matching on every possible table/type.

Is there any way to achieve this sort of code reuse?

1
(1) Shouldn’t your type be more like PersistentEntity a => Key a -> DBRequest (Maybe a)? (2) “…but that won't work if there's only a single constructor.” Why not?Alexis King
"The problem comes when trying to write the fetch function." - I don't see the problem (but maybe I don't have a good enough understanding of what you're trying to accomplish). Can you illustrate the problem with some code, i.e. with some code that you would like to be able to write but that doesn't compile (as well as the error the compiler gives)?user2407038
@AlexisKing that is much more sensible. I've made the edit.RichardW
@user2407038 I've added more detail and some sample code for what I would usually do in fetch. In short, the challenge is batching the BlockedFetches by the type a (which corresponds to a database table) so I can make a SQL query for each.RichardW

1 Answers

2
votes

I see two approaches:

Typeable:

data DBRequest a where
  RequestById :: (Typeable a, PersistEntity a) => Key a -> DBRequest (Maybe a)

or GADT "tag" type:

data Tag a where
    TagValue1 :: Tag Value1
    TagValue2 :: Tag Value2
    TagValue3 :: Tag Value3
    TagValue4 :: Tag Value4
    TagValue5 :: Tag Value5

data DBRequest a where
  RequestById :: PersistEntity a => Tag a => Key a -> DBRequest (Maybe a)

These are very similar patterns, especially If you use GHC-8.2, with https://hackage.haskell.org/package/base-4.10.1.0/docs/Type-Reflection.html (replace Tag a with TypeRep a).

Either way, you can group Key a using the tag. I haven't tried, but dependent-map might be handy: http://hackage.haskell.org/package/dependent-map