Agnishom Chattopadhyay
Agnishom Chattopadhyay

Reputation: 2041

The Haskell way to do IO Loops (without explicit recursion)?

I want to read a list of strings seperated by newlines from STDIN, until a new line is witnessed and I want an action of the type IO [String]. Here is how I would do it with recursion:

myReadList :: IO String
myReadList = go []
where 
    go :: [String] -> IO [String]   
    go l = do {
                 inp <- getLine;
                 if (inp == "") then 
                     return l;
                 else go (inp:l);
                }

However, this method of using go obscures readability and is a pattern so common that one would ideally want to abstract this out.

So, this was my attempt:

whileM :: (Monad m) => (a -> Bool) -> [m a] -> m [a]
whileM p []     = return []
whileM p (x:xs) = do
    s <- x
    if p s
    then do
        l <- whileM p xs
        return (s:l)
    else
        return []

myReadList :: IO [String]
myReadList = whileM (/= "") (repeat getLine)

I am guessing there is some default implementation of this whileM or something similar already. However I cannot find it.

Could someone point out what is the most natural and elegant way to deal with this problem?

Upvotes: 4

Views: 482

Answers (3)

danidiaz
danidiaz

Reputation: 27771

A solution using the effectful streams of the streaming package:

import Streaming
import qualified Streaming.Prelude as S

main :: IO ()
main = do
    result <- S.toList_ . S.takeWhile (/="") . S.repeatM $ getLine
    print result

A solution that shows prompts, keeping them separated from the reading actions:

main :: IO ()
main = do
    result <- S.toList_
            $ S.zipWith (\_ s -> s)
                        (S.repeatM $ putStrLn "Write something: ")
                        (S.takeWhile (/="") . S.repeatM $ getLine)
    print result

Upvotes: 1

Redu
Redu

Reputation: 26161

Yes for abstracting out the explicit recursion as mentioned in the previous answer there is the Control.Monad.Loop library which is useful. For those who are interested here is a nice tutorial on Monad Loops.

However there is another way. Previously, struggling with this job and knowing that Haskell is by default Lazy i first tried;

(sequence . repeat $ getLine) >>= return . takeWhile (/="q")

I expected the above to collect entered lines into an IO [String] type. Nah... It runs indefinitely and IO actişons don't look lazy at all. At this point System IO Lazy might come handy too. It's a 2 function only simple library.

run        :: T a -> IO a
interleave :: IO a -> T a

So run takes an Lazy IO action and turns it into an IO action and interleave does the opposite. Accordingly if we rephrase the above function as;

import qualified System.IO.Lazy as LIO

gls = LIO.run (sequence . repeat $ LIO.interleave getLine) >>= return . takeWhile (/="q")

Prelude> gls >>= return . sum . fmap (read :: String -> Int)
1
2
3
4
q
10

Upvotes: 1

Hogeyama
Hogeyama

Reputation: 758

unfoldWhileM is same as your whileM except that it takes an action (not a list) as second argument.

myReadList = unfoldWhileM (/= "") getLine

Upvotes: 13

Related Questions