Instantiating a typeclass based on a runtime value?

Previous Topic Next Topic
 
classic Classic list List threaded Threaded
3 messages Options
Reply | Threaded
Open this post in threaded view
|  
Report Content as Inappropriate

Instantiating a typeclass based on a runtime value?

Saurabh Nanda
Overall context: I'm trying to port the DelayedJob library from Ruby/Rails world to Haskell. The basic idea is to serialise a job and write it to the DB, deserialise it, and run the job using a job-runner function, which is determined by the job-type.

I'm using a type-class to define the custom job-runner, something on the lines of:

    class (FromJSON j, ToJSON j) => DelayedJob j where
      runJob :: j -> AppM ()

    data SyncContactsJob = SyncContactsJob { userId :: UserId } deriving (Eq, Show, Generic, FromJSON, ToJSON)

    instance DelayedJob SyncContactsJob where
      runJob job = do
        -- entire job execution logic comes here

Is there **any** type-system hackery, that will let me take a runtime value, eg. "SyncContactsJob", "DoBackupJob", and use that to run the correct version of the `runJob` function? That is,  how do I write the following function:

   invokeJob :: JobId -> AppM ()
   invokeJob jid = do
       jobRow <- fetchJob jid
       let jtype = jobRow ^. jobtype -- this will have "SyncContactsJob"
            jvalue = jobRow ^. jobdata -- this will have a Data.Aeson.Value
       runJob (......) -- QUESTION: What do I write here to make this compile?

_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.
Reply | Threaded
Open this post in threaded view
|  
Report Content as Inappropriate

Re: Instantiating a typeclass based on a runtime value?

Matt
You are asking GHC to select an instance of a type class (a compile-time operation) at run-time. I believe that this is possible with something like the exinst[1] package and runtime dictionaries with the constraints[2] package, but you're getting into some fairly hairy territory. I don't think that this would end up providing a nice UX for library consumers, either, as you end up needing user-defined singleton types to lift that information into the type level -- eg, a sum type of operations that the user supports, and then an open data family (or GADT) indexed by that sum type. In order for runJob to know about it, the class definition needs to know about that index, which kind of ruins the point of the class -- if you're going to keep a closed type of operations, you might as well just have:

data Job = SyncContact SyncContactJob | ImportFoo ImportFooJob

data SyncContactJob = SyncContactJob { userId :: UserId }
data ImportFooJob = ImportFooJob { fooId :: FooId }

runJob :: Job -> App ()

The main benefit to the existentialized approach, IMO, is when the actions are entirely derived from type class operations. Sandy Maguire gave a good talk on the approach at Lambdaconf [3]. Since much of the behavior here is ad hoc, then I think you'll get less utility out of the class.

We have a similar sort of framework on our projects at work, but using Amazon SQS messages instead of a database. The function has a signature like:

pollSqsFor :: FromJSON a => SqsQueue -> (a -> App b) -> App ()

This lets us write `pollSqsFor ImportThing (thingImporter :: ThingRequest -> App ())` for the workers that end up processing it, with the polling function handling stuff like delay time, error handling, keeping the message invisible, deleting it if successful, etc. This sort of thing would be easy to port to using a database, the only difference being loading only matching rows from the database for the given type.


Matt Parsons

On Wed, Jul 5, 2017 at 6:56 AM, Saurabh Nanda <[hidden email]> wrote:
Overall context: I'm trying to port the DelayedJob library from Ruby/Rails world to Haskell. The basic idea is to serialise a job and write it to the DB, deserialise it, and run the job using a job-runner function, which is determined by the job-type.

I'm using a type-class to define the custom job-runner, something on the lines of:

    class (FromJSON j, ToJSON j) => DelayedJob j where
      runJob :: j -> AppM ()

    data SyncContactsJob = SyncContactsJob { userId :: UserId } deriving (Eq, Show, Generic, FromJSON, ToJSON)

    instance DelayedJob SyncContactsJob where
      runJob job = do
        -- entire job execution logic comes here

Is there **any** type-system hackery, that will let me take a runtime value, eg. "SyncContactsJob", "DoBackupJob", and use that to run the correct version of the `runJob` function? That is,  how do I write the following function:

   invokeJob :: JobId -> AppM ()
   invokeJob jid = do
       jobRow <- fetchJob jid
       let jtype = jobRow ^. jobtype -- this will have "SyncContactsJob"
            jvalue = jobRow ^. jobdata -- this will have a Data.Aeson.Value
       runJob (......) -- QUESTION: What do I write here to make this compile?

_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.


_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.
Reply | Threaded
Open this post in threaded view
|  
Report Content as Inappropriate

Re: Instantiating a typeclass based on a runtime value?

Rein Henrichs
The job type is not a type in the Haskell sense. It is a just a value. Promoting it to a type just creates the problem you are trying to solve. The mechanism in Haskell that selects some value given another value is the function so why not write a function JobType -> Job -> App () that selects the right job running function for the given job type?

On Wed, Jul 5, 2017 at 10:11 AM Matt <[hidden email]> wrote:
You are asking GHC to select an instance of a type class (a compile-time operation) at run-time. I believe that this is possible with something like the exinst[1] package and runtime dictionaries with the constraints[2] package, but you're getting into some fairly hairy territory. I don't think that this would end up providing a nice UX for library consumers, either, as you end up needing user-defined singleton types to lift that information into the type level -- eg, a sum type of operations that the user supports, and then an open data family (or GADT) indexed by that sum type. In order for runJob to know about it, the class definition needs to know about that index, which kind of ruins the point of the class -- if you're going to keep a closed type of operations, you might as well just have:

data Job = SyncContact SyncContactJob | ImportFoo ImportFooJob

data SyncContactJob = SyncContactJob { userId :: UserId }
data ImportFooJob = ImportFooJob { fooId :: FooId }

runJob :: Job -> App ()

The main benefit to the existentialized approach, IMO, is when the actions are entirely derived from type class operations. Sandy Maguire gave a good talk on the approach at Lambdaconf [3]. Since much of the behavior here is ad hoc, then I think you'll get less utility out of the class.

We have a similar sort of framework on our projects at work, but using Amazon SQS messages instead of a database. The function has a signature like:

pollSqsFor :: FromJSON a => SqsQueue -> (a -> App b) -> App ()

This lets us write `pollSqsFor ImportThing (thingImporter :: ThingRequest -> App ())` for the workers that end up processing it, with the polling function handling stuff like delay time, error handling, keeping the message invisible, deleting it if successful, etc. This sort of thing would be easy to port to using a database, the only difference being loading only matching rows from the database for the given type.


Matt Parsons

On Wed, Jul 5, 2017 at 6:56 AM, Saurabh Nanda <[hidden email]> wrote:
Overall context: I'm trying to port the DelayedJob library from Ruby/Rails world to Haskell. The basic idea is to serialise a job and write it to the DB, deserialise it, and run the job using a job-runner function, which is determined by the job-type.

I'm using a type-class to define the custom job-runner, something on the lines of:

    class (FromJSON j, ToJSON j) => DelayedJob j where
      runJob :: j -> AppM ()

    data SyncContactsJob = SyncContactsJob { userId :: UserId } deriving (Eq, Show, Generic, FromJSON, ToJSON)

    instance DelayedJob SyncContactsJob where
      runJob job = do
        -- entire job execution logic comes here

Is there **any** type-system hackery, that will let me take a runtime value, eg. "SyncContactsJob", "DoBackupJob", and use that to run the correct version of the `runJob` function? That is,  how do I write the following function:

   invokeJob :: JobId -> AppM ()
   invokeJob jid = do
       jobRow <- fetchJob jid
       let jtype = jobRow ^. jobtype -- this will have "SyncContactsJob"
            jvalue = jobRow ^. jobdata -- this will have a Data.Aeson.Value
       runJob (......) -- QUESTION: What do I write here to make this compile?

_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.

_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.

_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.
Loading...