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

130 Views Asked by At

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?

2

There are 2 best solutions below

4
HTNW On BEST ANSWER

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"
4
leftaroundabout On

This is too long for a comment, but a necessary remark.

Your description strongly suggests that you should not dabble with any IO shenanigans to store those values. You don't even need to memoise function results (though that also can be done in a pure fashion, e.g. with MemoTrie).

Instead, you should simply flip the arguments around and then exploit partial application.

doTheWork :: TheInput -> Int -> MyResult
doTheWork inpData
       = \counter -> cheapSpecificComputation counter preciousSharedValue
 where preciousSharedValue = expensiveGeneralComputation inpData

This can then be used like

map (doTheWork constantInput) [LONG LIST OF COUNTERS]

where preciousSharedValue will only be computed once and then re-used for all the list. But unlike with a global mutable reference, you don't need to worry about all the things that can go wrong like incorrectly switching context between different TheInput values. Laziness, parallelism and more make explicit mutable storage a nightmare.

The flipped order of arguments seems clearly the appropriate one for the way the function is going to be used. You should change the signature accordingly. Changing even a large code base to adjust for such a change is pretty easy in Haskell, since the compiler will check exactly where a call to the function needs to be modified.

If this is a library that needs to preserve its interface, then the way to go is to give the flipped version a new name, implement the old-signature one in terms of it and deprecate it in favour of the flipped version:

workTheDo :: TheInput -> Int -> MyResult
workTheDo inpData = ...

{-# DEPRECATED doTheWork "Use workTheDo" #-}
doTheWork :: Int -> TheInput -> MyResult
doTheWork = flip workTheDo

Here, doTheWork will work as it did before (and not share the preciousSharedValue when mapped over a list).