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?
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