Scott
Scott

Reputation: 4110

Recursive IO in Haskell

In Haskell I can easily define a recursive function which takes a value and returns a string:

Prelude> let countdown i = if (i > 0) then (show i) ++ countdown (i-1) else ""
Prelude> countdown 5
"54321"

I want to use the same kind of design to read available data from a file handle. In this particular case I need to read the data in the same fashion as hGetContents, but without leaving the handle in the "semi-closed" state, so that I can loop interaction with stdin/stdout handles of a process opened with createProcess:

main = do
    -- do work to get hin / hout handles for subprocess input / output

    hPutStrLn hin "whats up?"

    -- works
    -- putStrLn =<< hGetContents hout

    putStrLn =<< hGetLines hout

    where
        hGetLines h = do
            readable <- hIsReadable h
            if readable
                then hGetLine h ++ hGetLines h
                else []

Gives the error:

Couldn't match expected type `IO b0' with actual type `[a0]'
In the expression: hGetLine h : hGetLines h

I know there are various libraries available for accomplishing what I'm trying to accomplish, but sice I'm learning my question is really how to perform recursive IO. TIA!

Upvotes: 5

Views: 4870

Answers (3)

Don Stewart
Don Stewart

Reputation: 137937

Naive solution, strict and O(n) stack

You still have to use the do-notation, which would lead to this:

import System.IO
import System.IO.Unsafe (unsafeInterleaveIO)

-- Too strict!
hGetLines :: Handle -> IO [String]
hGetLines h = do
    readable <- hIsReadable h
    if readable
        then do
            x  <- hGetLine h
            xs <- hGetLines h
            return (x:xs)
        else return []

But see my comment, this version of hGetLines is too strict!

Lazy, streaming version

It won't return your list, until it has all the input. You need something a bit lazier. For this, we have unsafeInterleaveIO,

-- Just right
hGetLines' :: Handle -> IO [String]
hGetLines' h = unsafeInterleaveIO $ do
    readable <- hIsReadable h
    if readable
        then do
            x  <- hGetLine h
            xs <- hGetLines' h
            return (x:xs)
        else return []

Now you can start streaming results line-by-line to your consumer code:

*Main> hGetLines' stdin
123
["123"345
,"345"321
,"321"^D^CInterrupted.

Upvotes: 12

Adam Bergmark
Adam Bergmark

Reputation: 7536

If you check the type of (++) in ghci you get:

Prelude> :t (++)
(++) :: [a] -> [a] -> [a]

Meaning you can only append lists together (Remember that String is an alias for [Char], so it's a list). The type of hGetLine is Handle -> IO String, and the type of hGetLines should be IO [String] So you can not append these values. (:) has type a -> [a] and works better here.

if readable
  then do
    -- First you need to extract them
    a <- hGetLine h
    b <- hGetLines h
    -- a and b have type String
    -- Now we can cons them and then go back into IO
    return (a : b)

The same applies for else []. You need a value of type IO [String] to be returned. Change it to return []

Also, you won't be able to just putStrLn the lines since (=<< hGetLines h) gives you [String] and not String which is what putStrLn expects. This can be solved in several ways. One is to concat the values first. putStrln . concat =<< (hGetLines h). Or you can print each line using mapM_ putStrLn (hGetLines h).

Upvotes: 6

Shea Levy
Shea Levy

Reputation: 5415

This is saying that part of the code expects hGetLines h to have type IO a and another part finds it to have type [a]. You probably want your if statement to be:

if readable
    then return hGetLine h ++ hGetLines h
    else return []

Upvotes: -1

Related Questions