I have a function
f :: MonadIO m => a -> m b
which takes some input and returns an IO computation that will yield output. I want to "memoize" f
so that I only ever perform these computations once for each input. For example, if
f :: String -> IO String
f s = putStrLn ("hello " ++ s) >> return s
then I want a function memoize
such that
do
mf <- memoize f
s <- mf "world"
t <- mf "world"
return (s,t)
prints "hello world"
exactly once and returns ("world", "world")
. The program I'm writing is multi-threaded, so this property should hold even if different threads are all calling mf
.
Below is the (terrible) solution I've come up with so far. My question is whether and how it can be improved.
memoize :: (MonadIO m, Ord a) => (a -> m b) -> m (a -> m b)
memoize f = do
cache <- liftIO $ newTVarIO Map.empty
return $ \a -> do
v <- liftIO $ atomically $ lookupInsert cache a
b <- maybe (f a) return =<< liftIO (atomically $ takeTMVar v)
liftIO $ atomically $ putTMVar v $ Just b
return b
where
lookupInsert :: Ord a => TVar (Map a (TMVar (Maybe b))) -> a -> STM (TMVar (Maybe b))
lookupInsert cache a = do
mv <- Map.lookup a <$> readTVar cache
case mv of
Just v -> return v
Nothing -> do
v <- newTMVar Nothing
modifyTVar cache (Map.insert a v)
return v
There are a few things going on here:
1) cache
has type TVar (Map a (TMVar (Maybe b)))
. It maps inputs to TMVar
's that contain either a computed value, or Nothing
(which indicates that a value hasn't been computed yet). The function lookupInsert
inspects cache
, and inserts a new TMVar
initialized to Nothing
if none is present already.
2) The returned action first obtains the v :: TMVar (Maybe b)
associated to a
, then takes it, and either performs the computation f a
to obtain a result or returns the value stored in the Maybe
if it's available. This take
and put
pattern is so that two different threads don't both run the computation f a
after seeing that it hasn't been run yet.
I thought what you wanted was impossible, but turns out it is.
https://stackoverflow.com/a/9458721/1798971
I still can't figure out why that works!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With