Learn You a Haskell for Great Good! - Miran Lipovaca [89]
Here’s an example:
ghci> let by = B.pack [98,111,114,116]
ghci> by
Chunk "bort" Empty
ghci> B.unpack by
[98,111,114,116]
You can also go back and forth between strict and lazy bytestrings. The toChunks function takes a lazy bytestring and converts it to a list of strict ones. The fromChunks function takes a list of strict bytestrings and converts it to a lazy bytestring:
ghci> B.fromChunks [S.pack [40,41,42], S.pack [43,44,45], S.pack [46,47,48]]
Chunk "()*" (Chunk "+,-" (Chunk "./0" Empty))
This is good if you have a lot of small strict bytestrings and you want to process them efficiently without joining them into one big strict bytestring in memory first.
The bytestring version of : is called cons. It takes a byte and a bytestring and puts the byte at the beginning.
ghci> B.cons 85 $ B.pack [80,81,82,84]
Chunk "U" (Chunk "PQRT" Empty)
The bytestring modules have a load of functions that are analogous to those in Data.List, including, but not limited to, head, tail, init, null, length, map, reverse, foldl, foldr, concat, takeWhile, filter, and so on. For a complete listing of bytestring functions, check out the documentation for the bytestring package at http://hackage.haskell.org/package/bytestring/.
The bytestring modules also have functions that have the same name and behave the same as some functions found in System.IO, but Strings are replaced with ByteStrings. For instance, the readFile function in System.IO has this type:
readFile :: FilePath -> IO String
The readFile function from the bytestring modules has the following type:
readFile :: FilePath -> IO ByteString
Note
If you’re using strict bytestrings and you attempt to read a file, all of that file will be read into memory at once! With lazy bytestrings, the file will be read in neat chunks.
Copying Files with Bytestrings
Let’s make a program that takes two filenames as command-line arguments and copies the first file into the second file. Note that System.Directory already has a function called copyFile, but we’re going to implement our own file-copying function and program anyway. Here’s the code:
import System.Environment
import System.Directory
import System.IO
import Control.Exception
import qualified Data.ByteString.Lazy as B
main = do
(fileName1:fileName2:_) <- getArgs
copy fileName1 fileName2
copy source dest = do
contents <- B.readFile source
bracketOnError
(openTempFile "." "temp")
(\(tempName, tempHandle) -> do
hClose tempHandle
removeFile tempName)
(\(tempName, tempHandle) -> do
B.hPutStr tempHandle contents
hClose tempHandle
renameFile tempName dest)
To begin, in main, we just get the command-line arguments and call our copy function, which is where the magic happens. One way to do this would be to just read from one file and write to another. But if something goes wrong (such as we don’t have enough disk space to copy the file), we’ll be left with a messed-up file. So we’ll write to a temporary file first. Then if something goes wrong, we can just delete that file.
First, we use B.readFile to read the contents of our source file. Then we use bracketOnError to set up our error handling. We acquire the resource with openTempFile "." "temp", which yields a tuple that consists of a temporary filename and a handle. Next, we say what we want to happen if an error occurs. If something goes wrong, we close the handle and remove the temporary file. Finally, we do the copying itself. We use B.hPutStr to write the contents to our temporary file. We close the temporary file and rename it to what we want it to be in the end.
Notice that we just used B.readFile and B.hPutStr instead of their regular variants. We didn’t need to use special bytestring functions for opening, closing, and renaming files. We just need to use the bytestring functions when reading and writing.
Let’s test it:
$ ./bytestringcopy bart.txt bort.txt
A program that doesn’t use