Kr0e
Kr0e

Reputation: 2249

Haskell processing [IO String]

I've got the following function:

lines' :: [IO String]
lines' = getLine : lines'

I was hoping I could just use all the mighty list functions on this list, like filter etc. But my knowledge about the IO monad in haskell is improvable.

The list-of-io_stuff-concept convinced me after using Rx for C#.

Is there any way to do what I want in haskell ? Something like:

ten_lines :: [IO String]
ten_lines = take 10 lines'

proc_lines :: [IO String]
proc_lines = [ (l, length l) | l <- lines' ]

Thanks!

Upvotes: 4

Views: 680

Answers (2)

Gabriella Gonzalez
Gabriella Gonzalez

Reputation: 35099

Tikhon's solution is the simplest one, but it has one major deficiency: it will not produce any results until processing the entire list and it will overflow if you process too large of a list.

A solution closer to C#'s Rx would be to use a streaming library like pipes.

For example, you can define a Producer that generates Strings from user input:

import Control.Monad
import Control.Proxy

lines' :: (Proxy p) => () -> Producer p String IO r
lines' () = runIdentityP $ forever $ do
    str <- lift getLine
    respond str

Then you can define a stage that takes 10 lines:

take' :: (Monad m, Proxy p) => Int -> () -> Pipe p a a m ()
take' n () = runIdentityP $ replicateM_ n $ do
    a <- request ()
    respond a

... and then a processing stage:

proc :: (Monad m, Proxy p) => () -> Pipe p String (String, Int) m r
proc () = runIdentityP $ forever $ do
    str <- request ()
    respond (str, length str)

... and a final output stage:

print' :: (Proxy p, Show a) => () -> Consumer p a IO r
print' () = runIdentityP $ forever $ do
    a <- request ()
    lift $ print a

Now you can compose those into a processing chain and run it:

main = runProxy $ lines' >-> take' 10 >-> proc >-> print'

... and it will output the processed result instantly after entering each line, rather than providing the result as a batch at the end:

$ ./pipes
Apple<Enter>
("Apple",5)
Test<Enter>
("Test",4)
123<Enter>
("123",3)
4<Enter>
("4",1)
5<Enter>
("5",1)
6<Enter>
("6",1)
7<Enter>
("7",1)
8<Enter>
("8",1)
9<Enter>
("9",1)
10<Enter>
("10",2)
$

In practice, you don't have to define these pipes yourself. You can assemble the same chain from components in the pipes standard library:

>>> runProxy $ stdinS >-> takeB_ 10 >-> mapD (\x -> (x, length x)) >-> printD
<exact same behavior>

Upvotes: 8

Tikhon Jelvis
Tikhon Jelvis

Reputation: 68172

There are a whole bunch of normal list functions modified to work with monads in Control.Monad. Of particular interest to your question:

sequence :: Monad m => [m a] -> m [a]
mapM     :: Monad m => (a -> m b) -> [a] -> m [b]
filterM  :: Monad m => (a -> m Bool) -> [a] -> m [a]
foldM    :: Monad m => (a -> b -> m a) -> a -> [b] -> m a

(sequence and mapM are actually exported by the prelude and available by default.)

For example, let's take a look at the type of your take 10 lines' example:

Prelude Control.Monad> :t take 10 lines'
take 10 lines' :: [IO String]

We want to turn this [IO String] into a single IO [String] action. This is exactly what sequence does! We can tell this by the type signature. So:

sequence $ take 10 lines'

will do what you want.

Most of these functions also have a version ending in _, like sequence_. This has exactly the same effect as the normal function except it throws away the result, returning () instead. That is, sequence_ :: [m a] -> m (). This is a good choice whenever you don't actually care about the result for two reasons: it's more explicit about your intentions and the performance can be better.

So if you wanted to print 10 lines rather than get them, you would write something like this:

printLines = putStrLn "foo" : printLines

main = sequence_ $ take 10 printLines

Upvotes: 10

Related Questions