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!