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 Functor
f`.
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