5

EDITED 2015-11-29: see bottom

I'm trying to write an application that has a do-last-action-again button. The command in question can ask for input, and my thought for how to accomplish this was to just rerun the resulting monad with memoized IO.

There are lots of posts on SO with similar questions, but none of the solutions seem to work here.

I lifted the memoIO code from this SO answer, and changed the implementation to run over MonadIO.

-- Memoize an IO function
memoIO :: MonadIO m => m a -> m (m a)
memoIO action = do
  ref <- liftIO $ newMVar Nothing
  return $ do
      x <- maybe action return =<< liftIO (takeMVar ref)
      liftIO . putMVar ref $ Just x
      return x

I've got a small repro of my app's approach, the only real difference being my app has a big transformer stack instead of just running in IO:

-- Global variable to contain the action we want to repeat
actionToRepeat :: IORef (IO String)
actionToRepeat = unsafePerformIO . newIORef $ return ""

-- Run an action and store it as the action to repeat
repeatable :: IO String -> IO String
repeatable action = do
    writeIORef actionToRepeat action
    action

-- Run the last action stored by repeatable
doRepeat :: IO String
doRepeat = do
    x <- readIORef actionToRepeat
    x

The idea being I can store an action with memoized IO in an IORef (via repeatable) when I record what was last done, and then do it again it out with doRepeat.

I test this via:

-- IO function to memoize
getName :: IO String
getName = do
    putStr "name> "
    getLine

main :: IO ()
main = do
    repeatable $ do
        memoized <- memoIO getName
        name <- memoized
        putStr "hello "
        putStrLn name
        return name
    doRepeat
    return ()

with expected output:

name> isovector
hello isovector
hello isovector

but actual output:

name> isovector
hello isovector
name> wasnt memoized
hello wasnt memoized

I'm not entirely sure what the issue is, or even how to go about debugging this. Gun to my head, I'd assume lazy evaluation is biting me somewhere, but I can't figure out where.

Thanks in advance!


EDIT 2015-11-29: My intended use case for this is to implement the repeat last change operator in a vim-clone. Each action can perform an arbitrary number of arbitrary IO calls, and I would like it to be able to specify which ones should be memoized (reading a file, probably not. asking the user for input, yes).

Community
  • 1
  • 1
  • Do you want to re-run the action, or do you want to return the result of the last action? This is an important difference. If the latter, you want to memoize the return value of the last action, while if the former, you want to remember the whole action. For example, if you want to read a file and return its content, do you want to read the file again (possibly getting updated data), or just return the cached content? – Petr Nov 29 '15 at 10:44
  • I want to replay the action (in arbitrary BigMonadStackT IO), with cached (returning just the result of) IO calls inside of it. The intended use case is to implement the [repeat last change](http://vim.wikia.com/wiki/Repeat_last_change) operator in vim, which asks for input only the first time you run it. – Sandy Maguire Nov 29 '15 at 15:35
  • I believe this gets a bit problematic. If you decide to memoize only some calls, what if the other, non-memoized calls, change the control flow? For example, what if the user's input is which file should be read? – Petr Nov 29 '15 at 20:26
  • This doesn't strike me as being problematic, but maybe I'm misguided about this. Compare two functions: `memoizedFilename = do { filename <- memoize getLine; readFile filename }` vs `memoizedFile = do { filename <- getLine; memoize $ readFile filename }` Here, memoizeFilename reads the file each time, but only prompts for the filename once, while memoizeFile prompts for a filename each time, but just returns the cached contents of the first file it was asked to. – Sandy Maguire Nov 30 '15 at 16:27

2 Answers2

5

the problem is in main you are creating a new memo each time you call the action

you need to move memoized <- memoIO getName up above the action

main :: IO ()
main = do
    memoized <- memoIO getName --moved above repeatable $ do
    repeatable $ do
                               --it was here 
        name <- memoized
        putStr "hello "
        putStrLn name
        return name
    doRepeat
    return ()

edit: is this acceptable

import Data.IORef
import System.IO.Unsafe

{-# NOINLINE actionToRepeat #-}
actionToRepeat :: IORef (IO String)
actionToRepeat = unsafePerformIO . newIORef $ return ""

type Repeatable a = IO (IO a)

-- Run an action and store the Repeatable part of the action
repeatable :: Repeatable String -> IO String
repeatable action = do
    repeatAction <- action
    writeIORef actionToRepeat repeatAction
    repeatAction

-- Run the last action stored by repeatable
doRepeat :: IO String
doRepeat = do
    x <- readIORef actionToRepeat
    x

-- everything before (return $ do) is run just once
hello :: Repeatable String
hello = do
    putStr "name> "
    name <- getLine
    return $ do
        putStr "hello "
        putStrLn name
        return name

main :: IO ()
main = do
    repeatable hello
    doRepeat
    return ()
No_signal
  • 402
  • 3
  • 6
  • Cool, thanks for the answer! Unfortunately, I'm not sure that's going to scale to my real application (a vim clone), though. Is there a way to define them in place like in my example? Some of my actions are recursive, with each iteration performing an IO that I'd like to memoize. One action takes input from the user getChar-by-getChar until the user decides to stop, and I'd like to be able to replay that in entirety. – Sandy Maguire Nov 29 '15 at 15:33
0

I came up with a solution. It requires wrapping the original monad in a new transformer which records the results of IO and injects them the next time the underlying monad is run.

Posting it here so my answer is complete.

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}

import Control.Applicative (Applicative(..))
import Data.Dynamic
import Data.Maybe (fromJust)
import Control.Monad.RWS

-- | A monad transformer adding the ability to record the results
-- of IO actions and later replay them.
newtype ReplayT m a =
    ReplayT { runReplayT :: RWST () [Dynamic] [Dynamic] m a }
    deriving ( Functor
             , Applicative
             , Monad
             , MonadIO
             , MonadState  [Dynamic]
             , MonadWriter [Dynamic]
             , MonadTrans
             )

-- | Removes the first element from a list State and returns it.
dequeue :: MonadState [r] m
        => m (Maybe r)
dequeue = do
    get >>= \case
        []     -> return Nothing
        (x:xs) -> do
            put xs
            return $ Just x

-- | Marks an IO action to be memoized after its first invocation.
sample :: ( MonadIO m
          , Typeable r)
       => IO r
       -> ReplayT m r
sample action = do
    a <- dequeue >>= \case
        Just x  -> return . fromJust $ fromDynamic x
        Nothing -> liftIO action
    tell [toDyn a]
    return a

-- | Runs an action and records all of its sampled IO. Returns a
-- action which when invoked will use the recorded IO.
record :: Monad m
       => ReplayT m a
       -> m (m a)
record action = do
    (a, w) <- evalRWST (runReplayT action) () []
    return $ do
        evalRWST (runReplayT action) () w
        return a