As others have pointed out, there is no clean way to "simulate" IO if you already use things like getLine and putStrLn . You must change the greeter . You can use the hGetLine and hPutStr and set the IO fake Handle , or you can use the Clear Code using Free Monads .
It is much more complex, but more general and usually suitable for this kind of ridicule, especially when it becomes more complex. I will briefly explain this below, although the details are somewhat complex.
The idea is that you will create your own "fake IO" monad, which can be "interpreted" in several ways. The primary interpretation is to use regular IO . A mocking interpretation replaces getLine with some fake lines and drives everything to stdout .
We will use the free package. The first step is to describe your interface using Functor . The basic concept is that each command is a branch of your data functor type and that the functor's “slot” is the “next action”.
{-
These constructors are almost like normal IO functions from the perspective of someone building a FakeIOF if you ignore the following action. If we want PutStr , we must provide String . If we want getLine , we provide a function that gives the next action when specifying a String .
Now we need to confuse the template a bit. We use the liftF function to turn our functor into a FreeT monad. Note that we provide () as the next action on PutStr and id as our String -> next function. It turns out that they give us the correct "return values" if we think about how our FakeIO Monad will behave.
Using them, we can build any functionality that we like and rewrite greeter with minimal changes.
fPutStrLn :: String -> FakeIO () fPutStrLn s = fPutStr (s ++ "\n") greeter :: FakeIO () greeter = do fPutStr "What your name? " name <- fGetLine fPutStrLn $ "Hi, " ++ name
This may seem a little magical - we use the do notation without specifying a Monad instance. The trick is that FreeT fm is Monad for any Monad m and Functor f`.
This concludes our greeter . Now we must interpret it one way or another, since we practically did not implement the functionality. To write an interpreter, we use the iterT function from Control.Monad.Trans.Free . This completely general type is as follows
iterT :: (Monad m, Functor f) => (f (ma) -> ma) -> FreeT fma -> ma
But when we apply it to our FakeIO monad, it looks
iterT :: (FakeIOF (IO a) -> IO a) -> FakeIO a -> IO a
which is much nicer. We provide him with a function that accepts FakeIOF functors filled with IO actors in the “next action” position (as it gets its name), to a simple IO action and iterT will do the magic of turning FakeIO into a real IO .
For our default interpreter, this is very simple.
interpretNormally :: FakeIO a -> IO a interpretNormally = iterT go where go (PutStr s next) = putStr s >> next -- next :: IO a go (GetLine doNext) = getLine >>= doNext -- doNext :: String -> IO a
But we can also make a mocking interpreter. We will use IO tools to store some state, in particular, a cyclic queue of fake responses.
newQ :: [a] -> IO (IORef [a]) newQ = newIORef . cycle popQ :: IORef [a] -> IO a popQ ref = atomicModifyIORef ref (\(a:as) -> (as, a)) interpretMocked :: [String] -> FakeIO a -> IO a interpretMocked greetings fakeIO = do queue <- newQ greetings iterT (go queue) fakeIO where go _ (PutStr s next) = putStr s >> next go q (GetLine getNext) = do greeting <- popQ q -- first we pop a fresh greeting putStrLn greeting -- then we print it getNext greeting -- finally we pass it to the next IO action
and now we can check these functions
λ> interpretNormally greeter What your name? Joseph Hi, Joseph. λ> interpretMocked ["Jabberwocky", "Frumious"] (greeter >> greeter >> greeter) What your name? Jabberwocky Hi, Jabberwocky What your name? Frumious Hi, Frumious What your name? Jabberwocky Hi, Jabberwocky