Search code examples
haskelliolazy-evaluationprocessing-efficiency

Is getLine lazy?


Is getLine lazy?

Say I have a very long line on the input. It's just a sequence of numbers. I only need to sum 3 first numbers. Will getLine be efficient and read only the first part of the line, or do I have to create my own function for lazy line reading, that would read characters one by one?

Will my implementation be efficient if I were to sum the whole line? (Will there be an overhead due to reading characters one by one?)

import Control.Applicative

main = do
    line <- getLine'
    print $ sum $ map read $ take 3 $ words line

getLine' :: IO String
getLine' = do
    c <- getChar
    if c == '\n' then return [] else (c:) <$> getLine'

Solution

  • While getLine isn't lazy, getContents is, and it can be combined with functions like lines and words. Therefore, the following program will only read enough of stdin to get (up to) three integers from the first line and print their sum:

    main :: IO ()
    main = do contents <- getContents
              let lns = lines contents
                  result = sum $ map read $ take 3 $ words $ head lns
              print (result :: Integer)
    

    Note that, if you modify the program to access subsequent lines -- for example, if you added:

    putStrLn $ take 80 $ lns !! 1
    

    to the bottom of the program to print the first 80 characters of the second line, then the program would have to finish reading the first line (and so would hang for a bit between the last two lines of the program) before processing the first 80 characters of the second. In other words, this lazy line reading is only useful if you only need to read the first bit of the first line, if that wasn't obvious to you -- Haskell doesn't have any magic way to skip the rest of the first line to get to the second.

    Finally, note that, for the above program, if there are fewer than three integers on the first line, it'll just sum those numbers and won't try to read past the first line (which I think is what you wanted). If you don't actually care about the line endings and just want to sum the first three numbers in the file, regardless of how they're divided up into lines, then you can break the contents up directly into words like so:

    main = do contents <- getContents
              let result = sum $ map read $ take 3 $ words contents
              print (result :: Integer)