Doing it the way you are trying to do it breaks the IO abstraction. In order to do it you'd have to use unsafe functions. Unsafe functions are bad. I'm not going to explain why but they tend to bite you as your program gets more complex and weirdness starts to occur, like threads ceasing operation while awaiting input is something that bit me when I went down that route. So let me explain how I would do it using both pipes and conduits as examples:
import Data.Conduit as C hiding ((>+>), runPipe)
import System.IO
import Control.Monad.Trans
import Text.Printf.Mauke
import Control.Pipe as P
import Control.Monad (forever)
-- Source runs in the IO monad and produces Strings
commandSource :: Source IO String
commandSource = do
command <- liftIO getLine
if command == "exit"
then return ()
else do
C.yield command
commandSource -- loop to fetching new values to send down the pipe
-- Sink runs in the IO monad and takes any printfable argument and returns () when pipe completes.
displaySink :: PrintfArg a => Sink a IO ()
displaySink = do
m <- C.await
case m of
Nothing -> return () -- if nothing comes in, just exit
Just x -> do
liftIO $ printf "Command not implemented (conduit): '%s'\n" x
displaySink
main = do
hSetBuffering stdout NoBuffering
commandSource $$ displaySink
runPipe $ commandProducer >+> displayConsumer
commandProducer :: PrintfArg a => Producer a String IO ()
commandProducer = do
x <- lift getLine
if x == "exit"
then return ()
else P.yield x >> commandProducer
displayConsumer :: Consumer String IO ()
displayConsumer = forever $ P.await >>= lift . printf "Command not implemented (pipes): '%s'\n"
There are some utility function to shorten some of these definitions a bit in conduit. These two examples are equivalent. But basically you are creating a pipeline, the first of which gets commands until it gets an exit and then sends them down the pipeline (as a string). The second piece of the pipe accepts anything that is printfable and prints it. It will stop when the upstream stops sending it strings to print. The point here is that you have little functions that you can compose together with other functions and create something bigger where none of the pieces interfere with each other or break the IO abstraction.
As to which of these libraries you should try? Conduits is a bit more straight forward and has a lot more documentation and supporting libraries. Pipes is a lot more flexible in that you could send things both directions along the pipe in the future when you become proficient with the library.