Sorry if this is a common question. I have this simple IO() function:
greeter :: IO()
greeter = do
putStr "What's your name? "
name <- getLine
putStrLn $ "Hi, " ++ name
Now I want to call greeter and at the same time specify a parameter that will pre-fill the getLine, so that I don't actually need to interact. I imagine something like a function
IOwithinputs :: [String] -> IO() -> IO()
then I'd do
IOwithinputs ["Buddy"] greeter
which would produce an IO action requiring no user input that would look something like:
What's your name?
Hi, Buddy
I want to do this without modifying the original IO() function greeter. I also don't want to compile greeter and pipe input from the command line. I don't see anything like IOwithinputs in Hoogle. (withArgs is tantalizingly typed and named, but isn't at all what I want.) Is there an easy way to do this? Or is it impossible for some reason? Is this what Pipes is for?
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
I don't think it is easy to do as you ask, but you can do next:
greeter' :: IO String -> IO()
greeter' ioS = do
putStr "What's your name? "
name <- ioS
putStrLn $ "Hi, " ++ name
greeter :: IO ()
greeter = greeter' getLine
ioWithInputs :: Monad m => [a] -> (m a -> m ()) -> m()
ioWithInputs s ioS = mapM_ (ioS.return) s
and test it:
> ioWithInputs ["Buddy","Nick"] greeter'
What's your name? Hi, Buddy
What's your name? Hi, Nick
and even more funny with emulation answer:
> ioWithInputs ["Buddy","Nick"] $ greeter' . (\s -> s >>= putStrLn >> s)
What's your name? Buddy
Hi, Buddy
What's your name? Nick
Hi, Nick
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