String concatenation with each line in a text file in Haskell

Viewed 168

I'm trying to write a code that'd read a given text file as its input and deliver the same text file with the length of each String at each line concatenated with the length of that String.

I have created a text file with a single String per line for this purpose. I've managed to write a code that gets a single line from the text file and outputs that line with its length shown as part of it, but I couldn't write a recursive version of this code, so that it'd continue doing this with every single line of the text file until there are no more lines left. It wouldn't have been a problem had I been working with lists but I can't use pattern matching with a text file full of Strings.

I just need to get the second code sample to apply itself to the entire list, but I can't. How can I change my code so that it'd work, without using functors/fmap? I'm really sorry for this stupid question, I'm fairly new to programming.

import System.IO
main :: IO()
main = do
   file <- openFile ".txt" ReadMode
   x <- hGetContents file >>=
   xs <- hGetLine 
   if null xs 
    then return ()
    else do
        putStrLn $ xs ++ " has a length of " ++ show (length xs)
   hClose file

or

main :: IO()
main = do
file <- openFile ".txt" ReadMode
x <- hGetLine file
xs <- hGetLine file
if null x 
  then return ()
  else do 
      putStrLn $ x ++ " has a length of " ++ show (length x)
      putStrLn $ xs ++ " has a length of " ++ show (length xs)
 hClose file
2 Answers

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'

You seem to want a recursive solution. But the main action is not well suited to call itself recursively, as it has specific duties that happen once: opening and closing the file.

So you need a separate recursive action, which assumes that file management is done by some layer above, and deals just with a pre-cooked file handle. Say we'll call it processFileHandle.

With that sort of type signature:

processFileHandle :: Handle -> IO ()  -- for now

But, hold on a second ! We have this text transformation to do:

xs ++ " has a length of " ++ show (length xs)

Do we want to hardwire this sort of code into our processFileHandle function ? Absolutely not ! We want to separate text processing from file I/O. That way, we eschew the necessity of re-writing processFileHandle every time the sort of line transformation we have to do changes.

So a much better type signature is:

processFileHandle :: (String -> String) ->  Handle -> IO ()

We provide the line transformation as an extra functional argument. In our case, this is:

transformLine1 :: String -> String
transformLine1 str =
    let  ln = length str
    in   str ++ " has a length of " ++ (show ln)

Now, to proceed with processFileHandle, we need a way to gracefully detect an end of file condition. But a function for this is bound to have the type signature : Handle -> IO Bool

So we submit this type signature into the Hoogle specialized search engine. And Hoogle points us towards the hIsEOF library function, which is just what we need.

We can now write our main action, this is just:

main :: IO ()
main = do
    fh <- openFile  "foo.txt"  ReadMode
    processFileHandle transformLine1 fh
    hClose fh

Now, we can provide the code for processFileHandle, as we can test for end of file:

processFileHandle :: (String -> String) -> Handle -> IO ()
processFileHandle fn fh =
  do
      atTheEnd <- hIsEOF fh    -- are we done ?
      if atTheEnd then
                      return ()  -- nothing left to do
                  else
                      do
                          line0 <- hGetLine fh
                          let  line1 = fn line0
                          putStrLn line1
                          processFileHandle fn fh  -- recursive call

Testing:

$ 
$ cat foo.txt
alpha
beta
epsilon
eta
$ 
$ ghc --version
The Glorious Glasgow Haskell Compilation System, version 8.8.4
$ 
$ ghc q68324502.hs -o ./q68324502.x
[1 of 1] Compiling Main             ( q68324502.hs, q68324502.o )
Linking ./q68324502.x ...
$ 
$ ./q68324502.x
alpha has a length of 5
beta has a length of 4
epsilon has a length of 7
eta has a length of 3
$ 
Related