Skip to content
AgileLogics
6 min read

Text, Files, and Directories

Prerequisites: IO and do-notation

Reading and writing files

main :: IO ()
main = do
  writeFile "notes.txt" "first line\nsecond line\n"
  appendFile "notes.txt" "third line\n"
  contents <- readFile "notes.txt"
  let ls = lines contents
  print (length ls)
  mapM_ (\(n, l) -> putStrLn (show n ++ ": " ++ l)) (zip [1 :: Int ..] ls)
3
1: first line
2: second line
3: third line

readFile reads lazily: the file is read as the string is consumed. That's convenient for big files, but it means you shouldn't write to a file while a lazy read of it is still in progress.

Data.Text

String is a linked list of characters — fine for learning, slow for real text. The text package (shipped with GHC) provides Text, a compact, fast Unicode string with a rich API:

{-# LANGUAGE OverloadedStrings #-}

import qualified Data.Text as T
import qualified Data.Text.IO as TIO

main :: IO ()
main = do
  let t = "  Hello, Haskell World  " :: T.Text
  TIO.putStrLn (T.strip t)
  TIO.putStrLn (T.toUpper (T.strip t))
  print (T.words "one two  three")
  print (T.splitOn "," "a,b,,c")
  print (T.replace "World" "Text" t)
  print (T.length t, T.isInfixOf "Haskell" t)
  TIO.putStrLn (T.intercalate " | " ["x", "y", "z"])
  putStrLn (T.unpack (T.take 5 (T.strip t)))
Hello, Haskell World
HELLO, HASKELL WORLD
["one","two","three"]
["a","b","","c"]
"  Hello, Haskell Text  "
(24,True)
x | y | z
Hello
  • OverloadedStrings lets string literals be Text (or any type with an IsString instance), so you don't have to call T.pack everywhere.
  • T.pack/T.unpack convert between String and Text.
  • Data.Text.IO reads and writes files strictly (TIO.readFile).
  • For binary data, use Data.ByteString from the bytestring package.

Files safely with handles

import System.IO

main :: IO ()
main = do
  withFile "log.txt" WriteMode $ \h -> do
    hPutStrLn h "started"
    hPutStrLn h "finished"
  withFile "log.txt" ReadMode $ \h -> do
    first <- hGetLine h
    eof <- hIsEOF h
    putStrLn (first ++ ", more lines: " ++ show (not eof))
started, more lines: True

withFile opens a handle, runs your code, and closes the handle even if an exception is thrown. The standard handles are stdin, stdout, and stderr — hPutStrLn stderr "message" writes an error message.

Directories

import System.Directory
import Data.List (sort)

main :: IO ()
main = do
  createDirectoryIfMissing True "data/archive"
  writeFile "data/a.txt" "A"
  writeFile "data/b.txt" "B"
  files <- listDirectory "data"
  print (sort files)
  doesFileExist "data/a.txt" >>= print
  renameFile "data/b.txt" "data/archive/b.txt"
  listDirectory "data/archive" >>= print
  removeDirectoryRecursive "data"
  doesDirectoryExist "data" >>= print
["a.txt","archive","b.txt"]
True
["b.txt"]
False

System.Directory comes from the directory package, and System.FilePath (</>, takeExtension, ...) from filepath — both ship with GHC.