Table of contents
Hello File
It’s easy to use Z-IO package’s filesystem module, first please import Z.IO.Filesystem
:
import qualified Z.IO.FileSystem as FS
If it’s OK to load a file into memory at once, you can use following:
readFile :: HasCallStack => CBytes -> IO Bytes
readTextFile :: HasCallStack => CBytes -> IO Text
writeFile :: HasCallStack => CBytes -> Bytes -> IO ()
writeTextFile :: HasCallStack => CBytes -> Text -> IO ()
CBytes
is Z’s file path type. Bytes
, and Text
are types for binary and textual content, respectively. These types are documented in Z-Data section. readTextFile
and writeTextFile
assumes UTF-8 encoding:
> FS.writeTextFile "./test_file" "hello world!"
> FS.readFile "./test_file"
[104,101,108,108,111,32,119,111,114,108,100,33]
> FS.readTextFile "./test_file"
"hello world!"
Resource Handling
Now let’s see a more complicated function:
initFile :: CBytes
-> FileFlag -- ^ Opening flags, e.g. 'O_CREAT' @.|.@ 'O_RDWR'
-> FileMode -- ^ Sets the file mode (permission and sticky bits),
-- but only if the file was created, see 'DEFAULT_FILE_MODE'.
-> Resource File
FileFlag
and FileMode
are bit constants controlling the file opening behavior, such as if we have read or write access or if a new file will be created when there’s none. You can find more constants on hackage docs. The interesting thing here is that initFile
function returns a Resource File
type instead of IO File
. Resource
is defined in Z.IO.Resource
module, with following functions to use it:
withResource :: HasCallStack
=> Resource a -- ^ resource management record
-> (a -> IO b) -- ^ function working on a resource
-> IO b
withResource' :: HasCallStack
=> Resource a -- ^ resource management record
-> (a -> IO () -> IO b)
-- ^ second param is the close function for early closing
-> IO b
We simplified those two functions’ type a little bit, and here is the idea: withResource
will take care of resource opening and cleanup automatically, after you finish using it, or when exceptions happen. You only need to pass a function working on that resource. Now let’s read the file created above again:
import Z.IO -- this module re-export Z.IO.Resource and other common stuff
import qualified Z.IO.FileSystem as FS
withResource (FS.initFile "./test_file" FS.O_RDWR FS.DEFAULT_FILE_MODE) $ \ file -> do
bi <- newBufferedInput file
printStd =<< readLine bi
initFile
function doesn’t open the file, and it just records how to open and close the file. Every time you want to do something with the file, use withResource
to open(and close) it, that’s all about resource handling in Z.
Resource
has a Monad
instance, which is useful for safely combining resources, e.g. instead of writing following code:
withResource initRes1 $ \ res1 ->
withResource initRes2 $ \ res2 ->
withResource initRes3 $ \ res3 ->
... res1 ... res2 ... res3
You could define a combined Resource
:
initRes123 :: Resource (Res1, Res2, Res3)
initRes123 = do
res1 <- initRes1
res2 <- initRes2
res3 <- initRes3
return (res1, res2, res3)
Now withResource initRes123 $ \ (res1, res2, res3) -> ...
will first open res1
, res2
, res3
in order, then close them in reverse order. You could even interleave IO
action within Resource
using its MonadIO
instance:
initRes123 :: Resource (Res1, Res2)
initRes123 = do
res1 <- initRes1
res2Param <- liftIO $ ... res1 ...
res2 <- initRes2 res2Param
return (res1, res2)
The lifted IO
action will become a part of the resource opening process.
Buffered I/O
newBufferedInput
and readLine
functions in the code above are from Z.IO.Buffered
module(also re-exported from Z.IO
). In Z-IO, many IO devices(including File
above) are instances of Input/Output
class:
class Input i where
readInput :: HasCallStack => i -> Ptr Word8 -> Int -> IO Int
class Output o where
writeOutput :: HasCallStack => o -> Ptr Word8 -> Int -> IO ()
readInput
and writeOutput
work on pointers, which is not very convenient for direct usage. Open a BufferedInput
or BufferedOutput
to get auto-managed buffered I/O:
newBufferedInput :: Input i => i -> IO BufferedInput
newBufferedOutput :: Output o => o -> IO BufferedOutput
There’s a set of functions working on BufferedInput/BufferedOutput
in Z.IO.Buffered
, for example, to implement a word counter for files:
import Z.IO
import qualified Z.IO.FileSystem as FS
import qualified Z.Data.Vector as V
main :: IO ()
main = do
-- get file path from command line
(_:path:_) <- getArgs
withResource (FS.initFile path FS.O_RDWR FS.DEFAULT_FILE_MODE) $ \ file -> do
bi <- newBufferedInput file
printStd =<< loop bi 0
where
loop :: BufferedInput -> Int -> IO Int
loop input !wc = do
-- read a single line with linefeed dropped
line <- readLine input
case line of
Just line' ->
loop input (wc + length (V.words line'))
_ -> return wc
Here’s a quick cheatsheet on buffered IO, BufferedInput
first:
-- | Request a chunk from the input device.
readBuffer :: HasCallStack => BufferedInput -> IO Bytes
-- | Push back an unconsumed chunk
unReadBuffer :: HasCallStack => Bytes -> BufferedInput -> IO ()
-- | Read exactly N bytes, throw exception if EOF reached before N bytes.
readExactly :: HasCallStack => Int -> BufferedInput -> IO Bytes
-- /----- readToMagic ----- \ /----- readToMagic -----\ ...
-- +------------------+-------+-----------------+-------+
-- | ... | magic | ... | magic | ...
-- +------------------+-------+-----------------+-------+
readToMagic :: HasCallStack => Word8 -> BufferedInput -> IO Bytes
-- /--- readLine ---\ discarded /--- readLine ---\ discarded / ...
-- +------------------+---------+------------------+---------+
-- | ... | \r\n/\n | ... | \r\n/\n | ...
-- +------------------+---------+------------------+---------+
readLine :: HasCallStack => BufferedInput -> IO (Maybe Bytes)
-- | Read all chunks from input.
readAll :: HasCallStack => BufferedInput -> IO [Bytes]
readAll' :: HasCallStack => BufferedInput -> IO Bytes
-- | See Parser & Builder under Z-Data section for the following functions.
-- | Request input using Parser
readParser :: HasCallStack => Parser a -> BufferedInput -> IO a
-- | Request input using ParseChunks, see Parser & Builder under Z-Data section.
readParseChunks :: (Print e, HasCallStack) => ParseChunks IO Bytes e a -> BufferedInput -> IO a
BufferedOutput
is relatively simple:
-- | Write a chunk into buffer.
writeBuffer :: HasCallStack => BufferedOutput -> Bytes -> IO ()
-- | Directly write Builder into output device.
writeBuilder :: HasCallStack => BufferedOutput -> Builder a -> IO ()
-- | Flush the buffer into output device.
flushBuffer :: HasCallStack => BufferedOutput -> IO ()
A note on filepath
Other operations from Z.IO.FileSystem
module, e.g., seek
, mkdtemp
, rmdir
, etc., are basically mirroring the Unix system call, which should be familiar to people who come from C/C++. The type for file path in Z is CBytes
, which is a \NUL
terminated byte array managed on GHC heap.
We assumed that CBytes
’s content is UTF-8 encoded though it may not always be the case, and there’re some platform differences on file path handling, e.g., the separator on windows is different from Unix. To proper handle file path, use Z.IO.FileSystem.FilePath
(which is re-exported from Z.IO.FileSystem
), for example, instead of manually connecting file path like:
let p = "foo" <> "/" <> "bar"
You should always use functions from the library
import qualified Z.IO.FileSystem as FS
let p = "foo" `FS.join` "bar"
-- "foo" `FS.join` "../bar" will yield "bar" instead of "foo/../bar"