Line 'em Up!

Reading from files and writing to files is a very important job that you'll have to do in a lot of programs. So it's very much worth investing your time in learning functions that will streamline that as much as possible.

Enter lines and unlines. These two functions make it substantially easier for you to deal with the separate lines of a file (or any other string really). Fundamentally, these two functions are inverses of each other. The "lines" function will take a single string, and return a list of strings, while "unlines" will take a list of strings and return a single string.

lines :: String -> [String]

unlines :: [String] -> String

Our friend "lines" takes a string that has newline characters (or carriage returns if you're using Windows) and then break it up by the lines.

>> lines "Hello\nWorld\n!"
["Hello", "World", "!"]

Then "unlines" is able to take that list and turn it back into a single string, where the entries of the list are separated by newlines.

>> unlines ["Hello", "World", "!"]
"Hello\nWorld\n!\n"

One distinction you'll notice is that our final string from "unlines" has an extra newline character. So strictly speaking, these functions aren't total inverses (i.e. unlines . lines /= id). But they still essentially function in this way.

As mentioned above, these functions are extremely handy for file reading and writing. Suppose we are reading a file and want to break it into its input lines. A naive solution would be to produce a Handle and then use iterated calls to hGetLine. But this gets tedious and is also error prone when it comes to our end condition.

readFileLines :: FilePath -> IO [String]
readFileLines fp = do
  handle <- openFile fp ReadMode
  results <- readLinesHelper handle []
  hClose handle
  return results
  where
    readLinesHelper :: Handle -> [String] -> IO [String]
    readLinesHelper handle prevs = do
      isEnd <- hIsEOF handle
      if isEnd
        then return (reverse prevs)
        else do
          nextLine <- hGetLine handle
          readLinesHelper handle (nextLine : prevs)

Boy that's a little messy…

And similarly, if we wanted to write each string in a list to a separate line in the file using hPutStrLn, this is non-trivial.

writeFileLines :: [String] -> FilePath -> IO ()
writeFileLines strings fp = do
  handle <- openFile fp WriteMode
  mapM_ (hPutStrLn handle) strings
  hClose handle

But these both get waaay easier if we think about using lines and unlines. We don't even have to think about the file handle! When reading a file, we just use lines in conjunction with readFile:

readLines :: FilePath -> IO [String]
readLines fp = lines <$> readFile fp

Then similarly, we use unlines with writeFile:

writeFileLines :: [String] -> FilePath -> IO ()
writeFileLines strings fp = writeFile fp (unlines strings)

And thus what were tricky implementations (especially for a beginner) are now made much easier!

Later this week we'll look at another set of functions that help us with this problem of splitting and fusing our strings. In the meantime, make sure to sign up for the Monday Morning Haskell newsletter if you haven't already! If you do, you'll get access to a lot of our great resources for beginning and intermediate Haskellers alike!

Previous
Previous

When Strings get Word-y

Next
Next

Classy Strings