Search code examples
haskellfunctional-programmingpersistencein-memory

How to persist an object in Haskell to a memory location via unsafePerformIO or similar


In a Haskell system I don't have much control over, I provide a syntactically pure function with the following signature:

doTheWork :: Int -> TheInput -> MyResult
doTheWork counter data = ...

This function does some work and returns its result. However the function is called again and again (until the system resource are used up) with incremented Ints to do more work on the same problem (the same TheInput data).

So to make use of the fact to be called again and again and not every time start again, I would need to store the intermediate MyResults. Of course, the "proper" semantically pure way would be that the system calls the function together with the old last results; i.e. the function signature would be something like doTheWork :: MyResult -> Int -> TheInput -> MyResult, but I have no control over the signature. The signature my function has to conform to is the shown at the beginning of the question! And I cannot move the function into the IO Monad of the program (because I don't have control over where the function is called).

So I was thinking to store the MyResults myself in memory via unsafePerformIO or similar. However I got stuck on the "safe to memory part". I can print the results out to stdout with something like

storeMyResults :: MyResults -> MyResults
storeMyResults data =
  unsafePerformIO $ do
    putStrLn show data)
    return  data

and then use storeMyResults data at the end of doTheWork when returning the results to write them out each time the function has computed new results.

But I have no idea how to write the MyResults value to memory and then read it out the next time I get called. I think I could write it to a file, but that's unnecessarily slow.

Is there a way to store a value in a memory location?


Solution

  • The typical way to do this is to unsafePerformIO yourself a top-level IORef. IORefs normally provide "mutable memory location" functionality within IO, but you can also break one out of IO.

    data MemoRecord = MemoRecord Int TheInput MyResult
    cacheCell :: IORef (Maybe MemoRecord)
    cacheCell = unsafePerformIO (newIORef Nothing)
    

    At this point, you have a mutable, program-global variable, like you would in C.

    Proceed to wire this into doTheWork. Note to take care to save the inputs as well as the output, so you can check you're doing the right thing. Again, do this in IO and then unsafePerformIO yourself out.

    -- really, it's up to you how you want to do this
    -- i'll just write something plausible so you know how
    
    doTheWork :: Int -> TheInput -> MyResult
    doTheWork i x = unsafePerformIO $ do
        r <- doTheWorkHinted i x <$> (check =<<) <$> readIORef cacheCell
        writeIORef cacheCell (Just (MemoRecord i x r))
        return r
      where
        -- check :: MemoRecord -> Maybe (Int, MyResult)
        check (MemoRecord i x' r)
          | x == x'   = Just (i, r) -- assuming Eq TheInput; otherwise you may have to resort to System.Mem.StableName, which can give false negatives
          | otherwise = Nothing
    
    -- given an int, the input, and maybe a previous result from the same input, find the result
    doTheWorkHinted :: Int -> TheInput -> Maybe (Int, MyResult) -> MyResult
    doTheWorkHinted = error "implement this"