haskelliobytestring

Deleting the last line of a file in Haskell


I am a beginner to Haskell and am trying to rewrite one of my shell scripts in Haskell as a test. One of the things I am trying to accomplish is removing the last line of a file and reading its value.

I tried to use readFile and then writeFile, but I can't because it is lazy. I was able to get it to work using this code:

import           Data.List
import qualified Data.ByteString.Char8         as B
import qualified Data.Text                     as T
import qualified Data.Text.Encoding            as TL

erase filename = do
  contents <- B.readFile filename
  let array = map (T.unpack . TL.decodeUtf8) (B.lines contents)
  B.writeFile filename $ B.pack (intercalate "\n" (init array))
  print $ last array

This turns the lines of the file into a list and then writes to the file without the last line. It is using the ByteString so it won't be lazy. However, I think that this way is very verbose and does it weirdly. Is there some function that deletes a line from a file or how can this code be improved?


Solution

  • I would advise not to write to the same file: most linux commands will normally always write to a different file, and optionally then move the target file over the original file. For example if you sort, you always have at least or the original file, or the result file.

    We can implement a helper function to obtain the content of the first lines and the last line:

    safeUnsnoc :: a -> [a] -> ([a], a)
    safeUnsnoc d = go
      where go [] = ([], d)
            go [x] = ([], x)
            go (x:xs) = (x:a, b) where ~(a, b) = go xs
    

    This is often better: it will avoid enumerating two times over the list, so this can be more memory conservative.

    Then we thus can read from the file, store the file in a temporary file, and do the file move:

    import System.Directory(renameFile)
    import System.IO.Temp(writeSystemTempFile)
    
    erase filename = do
      (content, lastLine) <- safeUnsnoc "" . lines <$> readFile filename
      tmp <- writeSystemTempFile "temp-init" (unlines content)
      renameFile tmp filename
      putStrLn lastLine