To write this recursively, you need a function to call itself. You already have a function main that could call itself, but you don't want to open the file multiple times, so it's better to split up the part that opens (and closes) the file from the part that reads lines using a helper function:
main :: IO ()
main = do
file <- openFile "test.txt" ReadMode
processLines
hClose file
Now we can write the recursive processLines function:
processLines :: Handle -> IO ()
processLines file = do
x <- hGetLine file
putStrLn $ x ++ " has a length of " ++ show (length x)
processLines file
This works, but it calls itself unconditionally, so it keeps reading lines until it hits the end of file and throws an exception. We can use the function hIsEOF to fix this:
processLines :: Handle -> IO ()
processLines file = do
eof <- hIsEOF file
if eof
then return ()
else do
x <- hGetLine file
putStrLn $ x ++ " has a length of " ++ show (length x)
processLines file
Because processLines is just a helper function and has no reasonable use outside of main, most Haskell programmers would pull it into main using a where clause or a let definition, and probably give it a shorter, throwaway name like loop or process or go. The result with a where clause is:
main :: IO ()
main = do
file <- openFile "test.txt" ReadMode
process file
hClose file
where
process file = do
eof <- hIsEOF file
if eof
then return ()
else do
x <- hGetLine file
putStrLn $ x ++ " has a length of " ++ show (length x)
process file
An advantage of using let is that you can define the helper with the file variable in scope, so you don't have to pass it around as an argument:
main :: IO ()
main = do
file <- openFile "test.txt" ReadMode
let loop = do
eof <- hIsEOF file
if eof
then return ()
else do
x <- hGetLine file
putStrLn $ x ++ " has a length of " ++ show (length x)
loop
loop
hClose file
Haskell provides more reasonable ways of writing this program in a loop form, avoiding explicit recursion and the need to define a helper. For example:
-- using `whileM_` from `monad-loops`
main1 :: IO ()
main1 = withFile "test.txt" ReadMode $ \h ->
whileM_ (not <$> hIsEOF h) $ do
x <- hGetLine h
putStrLn $ x ++ " has a length of " ++ show (length x)
or:
-- using `forM_` from `Control.Monad`
main2 :: IO ()
main2 = do
contents <- readFile "test.txt"
let lns = lines contents
forM_ lns $ \x ->
putStrLn $ x ++ " has a length of " ++ show (length x)
None of these are very good ways of writing this program, however. Fundamentally, you have a pure transformation for a line of input:
annotate :: String -> String
annotate x = x ++ " has a length of " ++ show (length x)
and you want to apply it to all lines in the file, which you can do with a one-liner (admittedly using fmap on the IO Functor):
main = putStr =<< unlines . map annotate . lines <$> readFile "test.txt"
or more explicitly as:
main = do
content <- readFile "test.txt"
let lns = lines content
lns' = [annotate l | l <- lns]
putStr $ unlines lns'