Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Haskell: carry out an IO action wrapped in a Data.Dynamic

Suppose I have a Data.Dynamic.Dynamic object which wraps an IO action (that is, something of type IO a for some perhaps-unknown a). I feel like I should be able carry out this IO action and get its result, wrapped in a Dynamic (which will have type a). Is there a standard library function which does this? (Something like dynApply, but for IO action performance instead of function application.)

The implementation of the function would perhaps look something like

dynPerform :: Dynamic -> Maybe IO Dynamic 
dynPerform (Dynamic typ act)
   = if (typeRepTyCon typ) /= ioTyCon then Nothing else Just $
       do result <- (unsafeCoerce act :: IO Any)
          return Just . Dynamic (head $ typeRepArgs typ) $ result

exampleIOAction = putChar
typeOfIOAction  = typeOf exampleIOAction
ioTyCon         = typeRepTyCon typeOfIOAction

but obviously this is uses several unsafe operations, so I'd rather pull it in from a library. (In fact, what I've written wouldn't work outside Data.Dynamic because of the opacity of the type Data.Dynamic.Dynamic.)

like image 351
circular-ruin Avatar asked Feb 19 '15 04:02

circular-ruin


1 Answers

I don't believe you can safely do what you are trying to do. Let me suggest an alternative approach.


Perhaps phantom types can help you here. Suppose you are providing some sort of cron job service, where the user has you perform an action every x microseconds, and the user can query at any time to see the result of the most recent run of that action.

Suppose you yourself have access to the following primitives:

freshKey :: IO Key
save :: Key -> Dynamic -> IO ()
load :: Key -> IO (Maybe Dynamic)

You should schedule the jobs and make a plan to store the results while you still "know" in the type system what type the action is.

-- do not export the internals of PhantomKey
data PhantomKey a = PhantomKey {
  getKey :: Key
  getThread :: Async ()
}

-- This is how your user acquires phantom keys;
-- their phantom type is tied to the type of the input action
schedule :: Typeable a => Int -> IO a -> IO (PhantomKey a)
schedule microseconds m = do
  k <- freshKey
  let go = do
        threadDelay microseconds
        a <- m
        save k (toDyn a)
        go
  thread <- async go
  return $ PhantomKey k thread

unschedule :: PhantomKey a -> IO ()
unschedule pk = cancel (getThread pk)

-- This is how your user uses phantom keys;
-- notice the function result type is tied to the phantom key type
peekLatest :: PhantomKey a -> IO (Maybe a)
peekLatest pk = load (getKey pk) >>= \md -> case md of
  Nothing -> return Nothing -- Nothing stored at this key (yet?)
  Just dyn -> case fromDynamic dyn of
    Nothing -> return Nothing -- mismatched data type stored at this key
                              -- hitting this branch is probably a bug
    Just a -> return (Just a)

Now if I'm a user of your API, I can use it with my own data types that you know nothing about, as long as they're Typeable:

refreshFoo :: IO Foo

main = do
  fooKey <- schedule 1000000 refreshFoo
  -- fooKey :: PhantomKey Foo
  mfoo <- peekLatest fooKey
  -- mfoo :: Maybe Foo

So what have we accomplished?

  • Your library is taking in a user IO action, and performing it at arbitrary points in time
  • Your library is saving your user's data via Dynamic blobs
  • Your library is loading your user's data via Dynamic blobs

All this without your library knowing anything about your user's data types.

It seems to me that if you are putting something which you know is an IO action into a Dynamic blob, you have lost information in the type system about that thing in a context when you should have instead made use of said type information. TypeRep can get you type information at the value level, but (as far as I know) cannot bubble that information back up into the type level.

like image 135
Dan Burton Avatar answered Nov 15 '22 05:11

Dan Burton