2

I'm just learning Haskell and I am trying to write some code that simply reads a file and creates a list of lines using the lines function. For example, I have a file called data.txt that contains the following lines:

this is line one
another line
and the final line

Here is the code I am trying to use to read this data into a list and print it to the screen:

import System.IO  
import Control.Monad

main = do  
        let list = []
        handle <- openFile "data.txt" ReadMode
        contents <- hGetContents handle
        let myLines = lines contents
            list = listLines myLines
        print list
        hClose handle   

listLines :: [String] -> [String]
listLines = map read

The resulting code compiles, but does not produce any output. I get the following output:

runhaskell test.hs        
read_file.hs: Prelude.read: no parse

Can anyone help me to understand what is wrong with my code? Thanks.

turtle
  • 7,533
  • 18
  • 68
  • 97
  • 2
    What do you expect `listLines` to do? – Vitus Mar 17 '12 at 21:23
  • The thing is, that `String → String` version of `read` expects Haskell string format (i.e. raw string like `"Hello \\" world \\""` and then turns it into `Hello " world "`). If you just want to print what you got from the file, remove `list = listLines myLines` completly. – Vitus Mar 17 '12 at 21:27
  • I expect `listLines` to take a list of strings and return a list of strings. Hmmm, I don't really understand what raw strings have to do with my code? Can you explain further? – turtle Mar 17 '12 at 21:31
  • Are you the same guy as in [this very similar question](http://stackoverflow.com/questions/7867723/haskell-file-reading)? – Daniel Wagner Mar 17 '12 at 21:36
  • 2
    `read` is supposed to take string representation of some data type and turn that into value of that type. So, for example `read "42" :: Int ≡ 42` or `read "[1,2]" :: [Int] ≡ [1,2]`. What is string representation of something, that is already a string? Well, you take string (for example `hello world`), add quotes, show special characters in their escaped form (i.e. newline → `\n`, `"` → `\"` and so). You want your Haskell code to `read` strings in exactly _this_ form; but that fails for obvious reasons. – Vitus Mar 17 '12 at 21:37
  • Yes, we know what type you expect `listLines` to have (since you included a type annotation in the code), but what do you expect it to _do_? – Daniel Wagner Mar 17 '12 at 21:38
  • @DanielWagner No, I am not the same guy; however, I did find this question, which I used as an example to for my code. – turtle Mar 17 '12 at 21:52
  • I expected listLines to put the lines of the files into a list of lines. – turtle Mar 17 '12 at 21:52

2 Answers2

8

As you might have noticed, the error message is telling you there's problem with read, so let's focus on that.

As I said in comments, read takes a string representation of a value of some data type, tries to parse it and return that value. Some examples:

read "3.14"    :: Double ≡ 3.14    :: Double
read "'a'"     :: Char   ≡ 'a'     :: Char
read "[1,2,3]" :: [Int]  ≡ [1,2,3] :: [Int]

Some nonexamples:

read "[1,2," :: [Int] ≡ error "*** Exception: Prelude.read: no parse"
read "abc"   :: Int   ≡ error "*** Exception: Prelude.read: no parse"

What happens when you try to use String version of read (i.e. read :: String → String)?

Haskell's representation of String (e.g. when you evaluate something that returns String in GHCi) consists of series of characters enclosed in " ... " quotes. Of course, if you want to show some special character (like newline), you have to put the escaped version in there (\n in this case).

Remember when I wrote that read expects a string representation of a value? In your case, read expects exactly this string format. Naturally, the first thing it tries to do is to match the opening quote. Since your first line doesn't begin with ", read complains and crashes the program.

read "hello" :: String fails in the same way that read "1" :: [Int] fails; 1 alone cannot be parsed as list of Ints - read expects the string to start with opening bracket [.


You might have also heard of show, which is the inverse (but in very loose sense) to read. As a rule of thumb, if you want to read a value x, the string representation read expects looks like show x.


If you were to change the content of the file to following

"this is line one"
"another line"
"and the final line"

your code would work just fine and produce following input:

["this is line one","another line","and the final line"]

If you don't want to change your .txt file, just remove list = listLines myLines and do print myLines. However, when you run the program, you'll get

["this is line one","another line","and the final line"]

again. So what's the issue?

print = putStrLn ∘ show and the default behaviour of show when it comes to showing list of something (i.e. [a] for some a; with exception of Char, which gets special treatment) is to produce string [ firstElement , secondElement ... lastElement ]. As you can see, if you want to avoid the [ ... ], you have to merge [String] back together.

There's nifty function called unlines, which is the inverse of lines. Also note, that print calls show first, but we do not want that in this case (we got the string we wanted already)! So we use putStrLn and we're done. Final version:

main = do  
    handle <- openFile "data.txt" ReadMode
    contents <- hGetContents handle
    let myLines = lines contents
    putStrLn (unlines myLines)
    hClose handle

We could also get rid of the unneeded lines ~ unlines and just putStrLn contents.

dflemstr
  • 25,947
  • 5
  • 70
  • 105
Vitus
  • 11,822
  • 7
  • 37
  • 64
  • 2
    Oh and just so you have something to look forward, this can be written as one-liner: `readFile "data.txt" >>= putStrLn`. – Vitus Mar 17 '12 at 23:33
2

hGetContents handle reads the contents of the handle into a string. To convert said string into a list of lines, you thus really want to split it on newlines. You don't need to deal with read -- you just want the string anyway. So you just need lines :: String -> [String]. Moreover, your code seems to indicate that you're "thinking imperatively", by "initializing" list before reading into it. Try something along the lines of

main = do 
  handle <- openFile "data.txt" ReadMode
  contents <- hGetContents handle 
  let list = lines contents
  print list
  hClose handle

There are some ways to make your program simpler, though. If you don't actually need the handle to the file for other stuff, you can easily get all the text in the file by simply calling readFile :: FilePath -> IO String. If you don't need list actually bound to a name, you can get rid of that as well, reducing your program to

main = do
  contents <- readFile "data.txt"
  print (lines contents)

To recap what happens here: "data.txt" is read, and its entire content is available as contents :: String. You want to turn this string into a list of strings, one for each line, i.e. exactly what lines contents is.

gspr
  • 11,144
  • 3
  • 41
  • 74
  • 1
    Mind you that this doesn't answer the problem he's having with his code. – Vitus Mar 17 '12 at 21:44
  • Oh, sorry, I must've misread. Hopefully he can extract something useful from it still, so I'll leave it hanging around. (Edit: Yep, I see it's about misunderstanding `read`. Oh well... I'll delete the answer if that's common etiquette) – gspr Mar 17 '12 at 21:47
  • 2
    There's no need to do that. You have quite a few useful thing in there. Haskellers are nice people, they won't downvote you because of that (I hope) :) – Vitus Mar 17 '12 at 21:51
  • 1
    Yes, please do not delete. This was very helpful! – turtle Mar 17 '12 at 23:15