Haskell Pipes in IO

Sorry if this is a general question. I have this simple IO() function:

 greeter :: IO() greeter = do putStr "What your name? " name <- getLine putStrLn $ "Hi, " ++ name 

Now I want to call greeter and at the same time specify a parameter that getLine pre-populates, so I actually don't need to interact. I represent something like a function

 IOwithinputs :: [String] -> IO() -> IO() 

then i would do

 IOwithinputs ["Buddy"] greeter 

which will result in an IO action that does not require user input, which will look something like this:

 What your name? Hi, Buddy 

I want to do this without changing the original IO() greeter . I also do not want to compile greeter and input channel from the command line. I don't see anything like IOwithinputs in Hoogle. ( withArgs painfully typed and named, but not at all what I want.) Is there an easy way to do this? Or is this impossible for some reason? Do you need Pipes for this?

+6
source share
3 answers

I don’t think it’s easy to do as you ask, but you can do the following:

 greeter' :: IO String -> IO() greeter' ioS = do putStr "What your name? " name <- ioS putStrLn $ "Hi, " ++ name greeter :: IO () greeter = greeter' getLine ioWithInputs :: Monad m => [a] -> (ma -> m ()) -> m() ioWithInputs s ioS = mapM_ (ioS.return) s 

and test it:

 > ioWithInputs ["Buddy","Nick"] greeter' What your name? Hi, Buddy What your name? Hi, Nick 

and even more fun answer with emulation:

 > ioWithInputs ["Buddy","Nick"] $ greeter' . (\s -> s >>= putStrLn >> s) What your name? Buddy Hi, Buddy What your name? Nick Hi, Nick 
+2
source

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”.

 {-# LANGUAGE DeriveFunctor #-} import Control.Monad.Trans.Free data FakeIOF next = PutStr String next | GetLine (String -> next) deriving Functor 

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.

 -- 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 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 
+2
source

Once you are in IO , you cannot change the input method. To answer your question about pipes , yes, you can abstract the input by specifying Consumer :

 import Pipes import qualified Pipes.Prelude as P greeter :: Consumer String IO () greeter = do lift $ putStr "What your name? " name <- await lift $ putStrLn $ "Hi, " ++ name 

Then you can specify to use the command line as input:

 main = runEffect $ P.stdinLn >-> greeter 

... or use a clean set of strings as input:

 main = runEffect $ each ["Buddy"] >-> greeter 
0
source

Source: https://habr.com/ru/post/957990/


All Articles