Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

haskell piping strings into IO

Tags:

io

haskell

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?

like image 942
user2744010 Avatar asked Nov 13 '13 12:11

user2744010


2 Answers

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
like image 170
J. Abrahamson Avatar answered Oct 13 '22 10:10

J. Abrahamson


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
like image 21
viorior Avatar answered Oct 13 '22 10:10

viorior