Julian
Julian

Reputation: 4666

How to use Haskell's readFile function for multiple files

I understand a little bit about IO. I understand that you can use readFile to get the content of a file. For example like this:

main = do
    let inputFilePath = "C:\\Haskell\\myawesomeprogram\\files\\a.txt"
    content <- readFile inputFilePath
    print content

Calling the program:

> runghc myawesomeprogram
"AAA"

Awesome, that works! Now I want to read the content from multiple files. I tried something like this:

files = ["C:\\Haskell\\myawesomeprogram\\files\\a.txt", "C:\\Haskell\\myawesomeprogram\\files\\b.txt","C:\\Haskell\\myawesomeprogram\\files\\c.txt"]


main :: IO ()
main = do
    filesContent <- readFiles files
    print filesContent

readFiles (x:xs) = do 
    content <- readFile x
    content : readFiles xs

This will give me the following error message:

myawesomeprogram.hs:6:21: error:
    * Couldn't match type `[]' with `IO'
      Expected type: IO String
        Actual type: [String]
    * In a stmt of a 'do' block: filesContent <- readFiles files
      In the expression:
        do filesContent <- readFiles files
           print filesContent
      In an equation for `main':
          main
            = do filesContent <- readFiles files
                 print filesContent
  |
6 |     filesContent <- readFiles files
  |                     ^^^^^^^^^^^^^^^

myawesomeprogram.hs:9:1: error:
    Couldn't match type `IO' with `[]'
    Expected type: [FilePath] -> [String]
      Actual type: [FilePath] -> IO String
  |
9 | readFiles (x:xs) = do
  | ^^^^^^^^^^^^^^^^^^^^^^^...

myawesomeprogram.hs:11:5: error:
    * Couldn't match type `[]' with `IO'
      Expected type: IO String
        Actual type: [String]
    * In a stmt of a 'do' block: content : readFiles xs
      In the expression:
        do content <- readFile x
           content : readFiles xs
      In an equation for `readFiles':
          readFiles (x : xs)
            = do content <- readFile x
                 content : readFiles xs
   |
11 |     content : readFiles xs
   |     ^^^^^^^^^^^^^^^^^^^^^^

I'm doing something wrong, however, I can't see a way to do it right. Can you do it the right way?

Upvotes: 3

Views: 207

Answers (2)

Steven Armstrong
Steven Armstrong

Reputation: 475

If you want to do something to every item in a list, you use map.

However, map readFile files over a list only gives you a list of actions that will produce their contents, rather than a list of their contents. There is then sequence which will take a list of actions, and turn it into an action that produces a list of their results. Mapping an action and sequencing it is a frequent enough concern that this is shortened to mapM. It is located in Control.Monad.

The code you want is thus:

main :: IO ()
main = do
    filesContent <- mapM readFile files
    print filesContent

Please do not roll your own function to do this.

Upvotes: 0

Fyodor Soikin
Fyodor Soikin

Reputation: 80714

readFiles xs is not a list, so you can't prepend an item to it. Instead, it's an action, which, when executed, would produce a list.

More specifically, the type of readFiles xs is IO [String] (IO is the type of executable actions), whereas a list would have type [String]. This is what the error message is telling you: cannot match type IO [String] with [String].

So to get the list, you have to execute the action, just like you're executing readFile

readFiles (x:xs) = do
    content <- readFile x
    theRest <- readFiles xs
    pure (content : theRest)

Also note that readFiles doesn't know what to do when its argument is an empty list. You should get a warning about it at compile time, and if you don't fix it, you'll get a crash at runtime.

To fix, just add an equation for the empty list case:

readFiles [] = pure []
readFiles (x:xs) = do
    content <- readFile x
    theRest <- readFiles xs
    pure (content : theRest)

Upvotes: 6

Related Questions