As others have noted, there's no clean way to "simulate" IO if you're already using things like getLine and putStrLn. You have to modify greeter. You could use the hGetLine and hPutStr versions and mock out IO with a fake Handle, or you could use the Purify Code with Free Monads method.
It's far more complex, but also more general and usually a good fit for this kind of mocking, especially when it gets more complex.. I'll explain it briefly below, though the details are somewhat sophisticated.
The idea is that you will be creating your own "fake IO" monad which can be "interpreted" in multiple ways. The primary interpretation is just to use regular IO. The mocked interpretation replaces getLine with some fake lines and echoes everything to stdout.
We'll use the free package. The first step is to describe your interface with a Functor. The basic notion is that each command is a branch of your functor data type and that the functor's "slot" represents the "next action".
{-# LANGUAGE DeriveFunctor #-}
import Control.Monad.Trans.Free
data FakeIOF next = PutStr String next
| GetLine (String -> next)
deriving Functor
These constructors are almost like the regular IO functions from the point of view of someone building a FakeIOF if you ignore the next action. If we want to PutStr we must provide a String. If we want to GetLine we provide a function with only gives the next action when given a String.
Now we need a little confusing boilerplate. We use the liftF function to turn our functor into a FreeT monad. Notice that we provide () as the next action on PutStr and id as our String -> next function. It turns out that these give us the right "return values" if we think of how our FakeIO Monad will behave.
-- Our FakeIO monad
type FakeIO a = FreeT FakeIOF IO a
fPutStr :: String -> FakeIO ()
fPutStr s = liftF (PutStr s ())
fGetLine :: FakeIO String
fGetLine = liftF (GetLine id)
Using these we can build whatever functionality we like and rewrite greeter with very minimal changes.
fPutStrLn :: String -> FakeIO ()
fPutStrLn s = fPutStr (s ++ "\n")
greeter :: FakeIO ()
greeter = do
fPutStr "What's your name? "
name <- fGetLine
fPutStrLn $ "Hi, " ++ name
This might look a little bit magical---we're using do notation without defining a Monad instance. The trick is that FreeT f m is a Monad for any Monad m and Functorf`.
This completes our "mocked" greeter function. Now we must interpret it somehow as we've implemented almost no functionality so far. To write an interpreter we use the iterT function from Control.Monad.Trans.Free. It's fully general type is as follows
iterT
:: (Monad m, Functor f) => (f (m a) -> m a) -> FreeT f m a -> m a
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 it a function that takes FakeIOF functors filled with IO actons in the "next action" position (which is how it gets its name) to a plain IO action and iterT will do the magic of turning FakeIO into real IO.
For our default interpreter this is really easy.
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 mocked interpreter. We'll use the facilities of IO 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 test these functions
λ> interpretNormally greeter
What's your name? Joseph
Hi, Joseph.
λ> interpretMocked ["Jabberwocky", "Frumious"] (greeter >> greeter >> greeter)
What's your name?
Jabberwocky
Hi, Jabberwocky
What's your name?
Frumious
Hi, Frumious
What's your name?
Jabberwocky
Hi, Jabberwocky
greeter, unfortunately.getLinewill always read fromstdin, which is what the user types. The easiest way to change it would be to usehGetLineinstead, which lets you specify whichHandleyou want to get the line from. It might also be possible (I'm very unsure about this) to do some low-level manipulation and set up a redirect yourself using theGHC.IO.Handlemodule, but it is obviously the last thing you want to do.stdin, but it doesn't let you), b ut this is not what thepipeslibrary is for.pipesis for writing a program where you separate your data generators from the data processing steps. You can use it with IO (and it often is), I would recommend the tutorialIO ()argument is supposed to deal with data from other Haskell functions, then why does it not accept it as a parameter?IOwithinputs :: [String] -> ([String] -> IO()) -> IO(). (Note that then, basicallyIOwithinputs ≡ flip ($).)