How to deserialise a JSON where the corresponding Haskell type is available during runtime as a value?

92 Views Asked by At

I'm trying to implement a port of DelayedJob (from the Rails world) in Haskell.

Here's the typeclass I have which represents a DelayedJob

class (FromJSON j, ToJSON j, FromJSON r, ToJSON r) => DelayedJob j r | j -> r where
  serialise :: j -> Value
  serialise = toJSON

  deserialise :: Value -> Either String j
  deserialise = parseEither parseJSON

  runJob :: j -> AppM r

Here's how I plan to use it:

createJob :: (DelayedJob j r) => j -> AppM JobId

I'm getting stuck with writing a fairly general invokeJob function which will read a row from the jobs table, look at the jobs.jobtype column and invoke the correct version of the runJob version (i.e. the runJob function belonging to the correct type-class instance).

I have the following, but it is full of boilerplate:

data JobType = SyncContacts | SendEmail | SendSms deriving (Eq, Show, Generic)

invokeJob :: JobId -> AppM ()
invokeJob jid = do
  job <- fetchJob jid
  case (job ^. jobtype) of
     SyncContacts -> case (deserialise (job ^. jobdata) :: Either String SynContactsJob) of
       Left e -> error e
       Right j -> storeJobResult jid $ runAppM j
     SendEmail -> case (deserialise (job ^. jobdata) :: Either String SendEmailJob) of
       Left e -> error e
       Right j -> storeJobResult jid $ runAppM j
     SendSms -> case (deserialise (job ^. jobdata) :: Either String SendSms) of
       Left e -> error e
       Right j -> storeJobResult jid $ runAppM j

Essentially, is there a way to constrain the concrete type of the deserialise function dynamically during runtime without having to write so much boilerplate?

2

There are 2 best solutions below

1
On

I am loath to suggest it, but IMHO the "right" thing to do here is to forego type safety with a little runtime polymorphism. Instead of a type class, have an opaque Handler of sorts, in your case it'd probably be something like Value -> AppM ().

Just put these in a Map Type Handler or something along those lines, which you can update dynamically; whereby Type is what you use to distinguish your job types (i.e. some sum type or, for minimum type safety, just a String). When you pull out a job you can just look up the appropriate handler in the list; The handler is opaque and knows which types it is responsible for, so the user end doesn't need to know how to deserialize values.

This isn't very clever but should work nevertheless.

1
On

I wonder if something like this could work. I don't know the framework you are working with.

let go :: forall jobtype . Job -> JobID -> ....
    go job jid =
       case deserialise (job ^. jobdata) :: Either String jobtype of
          Left e -> error e
          Right j -> storeJobResult jid $ runAppM j
case (job ^. jobtype) of
     SyncContacts -> go @ SynContactsJob job jid
     SendEmail    -> go @ SendEmail job jid
     ...

Otherwise, I'd try to see if singletons can help. Proxies could also be used if you don't want to enable ambiguous types & type applications.