packages feed

csv-enumerator (empty) → 0.8

raw patch · 6 files changed

+682/−0 lines, 6 filesdep +attoparsecdep +attoparsec-enumeratordep +basesetup-changed

Dependencies added: attoparsec, attoparsec-enumerator, base, bytestring, containers, directory, enumerator, safe, transformers, unix-compat

Files

+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c)2010, Ozgun Ataman++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Ozgun Ataman nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.markdown view
@@ -0,0 +1,94 @@+# README++## CSV Files and Haskell++CSV files are the de-facto standard in many cases of data transfer,+particularly when dealing with enterprise application or disparate database+systems.++While there are a number of csv libraries in Haskell, at the time of this+project's start in 2010, there wasn't one that provided all of the following:++* Full flexibility in quote characters, separators, input/output+* Constant space operation+* Robust parsing and error resiliency+* Fast operation+* Convenient interface that supports a variety of use cases++This library is an attempt to close this gap.+++## This package++csv-enumerator is an enumerator-based CSV parsing library that is easy to use,+flexible and fast. Furthermore, it provides ways to use constant-space during+operation, which is absolutely critical in many real world use cases.+++### Introduction++* ByteStrings are used for everything+* There are 2 basic row types and they implement *exactly* the same operations,+  so you can chose the right one for the job at hand:+  - type MapRow = Map ByteString ByteString+  - type Row = [ByteString]+* Folding over a CSV file can be thought of as the most basic operation.+* Higher level convenience functions are provided to "map" over CSV files,+  modifying and transforming them along the way.+* Helpers are provided for simple input/output of CSV files for simple use+  cases.+* For extreme / advanced use cases, the user can drop down to the+  Enumerator/Iteratee level and do interleaved IO among other things.++### API Docs++The API is quite well documented and I would encourage you to keep it handy.++### Speed++While fast operation is of concern, I have so far cared more about correct+operation and a flexible API. Please let me know if you notice any performance+regressions or optimization opportunities.+++### Usage Examples++#### Example 1: Basic Operation ++    {-# LANGUAGE OverloadedStrings #-}++    import Data.CSV.Enumerator+    import Data.Char (isSpace)+    import qualified Data.Map as M+    import Data.Map ((!))++    -- Naive whitespace stripper+    strip = reverse . B.dropWhile isSpace . reverse . B.dropWhile isSpace++    -- A function that takes a row and "emits" zero or more rows as output.+    processRow :: MapRow -> [MapRow]+    processRow row = [M.insert "Column1" fixedCol row]+      where fixedCol = strip (row ! "Column1")++    main = mapCSVFile "InputFile.csv" defCSVSettings procesRow "OutputFile.csv"++and we are done. +++Further examples to be provided at a later time.++++### TODO - Next Steps++* Need to think about specializing an Exception type for the library and+  properly notifying the user when parsing-related problems occur.+* Some operations can be further broken down to their atoms, increasing the+  flexibility of the library.+* The CSVeable typeclass can be refactored to have a more minimal definition.+* Operating on Text in addition to ByteString would be phenomenal.+* A test-suite needs to be added.+++Any and all kinds of help is much appreciated!+
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ csv-enumerator.cabal view
@@ -0,0 +1,41 @@+Name:                csv-enumerator+Version:             0.8+Synopsis:            A flexible, fast, enumerator-based CSV parser library for Haskell.  +Homepage:            http://github.com/ozataman/csv-enumerator+License:             BSD3+License-file:        LICENSE+Author:              Ozgun Ataman +Maintainer:          Ozgun Ataman <ozataman@gmail.com>+Category:            Data+Build-type:          Simple+Cabal-version:       >=1.2+Description:+  For more information and examples, check out the README at:+  <http://github.com/ozataman/csv-enumerator>.+  +  The API is fairly well documented and I would encourage you to keep your+  haddocks handy.+++extra-source-files:+  README.markdown++Library+  hs-source-dirs: src+  Exposed-modules:     +      Data.CSV.Enumerator+  Other-modules:+      Data.CSV.Enumerator.Types+  build-depends:+      attoparsec >= 0.8 && < 0.9+    , attoparsec-enumerator >= 0.2.0.3+    , base >= 4 && < 5+    , containers >= 0.3+    , directory+    , bytestring+    , enumerator >= 0.4.5 && < 0.5+    , transformers >= 0.2+    , safe+    , unix-compat >= 0.2.1.1+  extensions:+    ScopedTypeVariables
+ src/Data/CSV/Enumerator.hs view
@@ -0,0 +1,469 @@+{-# LANGUAGE OverloadedStrings, BangPatterns #-}+{-# LANGUAGE PackageImports #-}+{-# LANGUAGE TypeSynonymInstances #-}++module Data.CSV.Enumerator +  ( +   -- * CSV Data types+    Row   -- Simply @[ByteString]@+  , Field   -- Simply @ByteString@+  , MapRow  ++  , CSVeable(..)++  , ParsedRow(..) ++  -- * CSV Setttings+  , CSVSettings(..)+  , defCSVSettings++  -- * Reading / Writing CSV Files+  , readCSVFile+  , writeCSVFile+  , appendCSVFile++  -- * Folding Over CSV Files +  -- | These enumerators generalize the map* family of functions with a running accumulator.+  , CSVAction+  , funToIter+  , funToIterIO++  -- * Primitive Iteratees+  , collectRows+  , outputRowIter+  , outputRowsIter++  -- * Other Utilities+  , outputRow+  , outputRows+  , writeHeaders+  )++where++import Control.Applicative hiding (many)+import Control.Exception (bracket, SomeException)+import Control.Monad (mzero, mplus, foldM, when)+import Control.Monad.IO.Class (liftIO, MonadIO)+import qualified Data.ByteString as B+import qualified Data.ByteString.Char8 as B8+import Data.ByteString.Internal (c2w)+import qualified Data.Map as M+import System.Directory+import System.IO+import System.PosixCompat.Files (getFileStatus, fileSize)++import Data.Attoparsec as P hiding (take)+import qualified Data.Attoparsec.Char8 as C8+-- import Data.Attoparsec.Enum+import Data.Attoparsec.Enumerator+import qualified Data.Enumerator as E+import Data.Enumerator (($$), yield, continue)+import Data.Enumerator.Binary (enumFile)+import Data.Word (Word8)+import Safe (headMay)++import Data.CSV.Enumerator.Types++class CSVeable r where++  -- | Convert a CSV row into strict ByteString equivalent.+  rowToStr :: CSVSettings -> r -> B.ByteString++  -- | Possibly return headers for a list of rows.+  fileHeaders :: [r] -> Maybe Row++  -- | The raw iteratee to process any Enumerator stream+  iterCSV :: CSVSettings+          -> CSVAction r a+          -> a+          -> E.Iteratee B.ByteString IO a++  -- | Iteratee to push rows into a given file+  fileSink :: CSVSettings+           -> FilePath+           -> (Maybe Handle, Int)+           -> ParsedRow r+           -> E.Iteratee B.ByteString IO (Maybe Handle, Int)++  -- | Open & fold over the CSV file.  Processing starts on row 2 for MapRow+  -- instance to use first row as column headers.+  foldCSVFile :: FilePath -- ^ File to open as a CSV file+              -> CSVSettings -- ^ CSV settings to use on the input file+              -> CSVAction r a -- ^ Fold action+              -> a  -- ^ Initial accumulator+              -> IO (Either SomeException a) -- ^ Error or the resulting accumulator++  -- | Take a CSV file, apply function to each of its rows and save the+  -- resulting rows into a new file.+  --+  -- Each row is simply a list of fields.+  mapCSVFile :: FilePath         -- ^ Input file+             -> CSVSettings      -- ^ CSV Settings+             -> (r -> [r])       -- ^ A function to map a row onto rows+             -> FilePath         -- ^ Output file+             -> IO (Either SomeException Int)    -- ^ Number of rows processed +  mapCSVFile fi s f fo = do+    res <- foldCSVFile fi s iter (Nothing, 0)+    return $ snd `fmap` res+    where+      iter !acc (ParsedRow (Just !r)) = foldM chain acc (f r) +      iter !acc x = fileSink s fo acc x++      chain !acc !r = singleSink r acc++      singleSink !x !acc = fileSink s fo acc (ParsedRow (Just x))+++  ----------------------------------------------------------------------------+  -- | Like 'mapCSVFile' but operates on multiple files pouring results into+  -- a single file.+  mapCSVFiles :: [FilePath]       -- ^ Input files+              -> CSVSettings      -- ^ CSV Settings+              -> (r -> [r])       -- ^ A function to map a row onto rows+              -> FilePath         -- ^ Output file+              -> IO (Either SomeException Int)    -- ^ Number of rows processed ++------------------------------------------------------------------------------+-- | 'Row' instance for 'CSVeable'+instance CSVeable Row where+  rowToStr s !r = +    let +      sep = B.pack [c2w (csvOutputColSep s)] +      wrapField !f = case (csvOutputQuoteChar s) of+        Just !x -> let qt = c2w x+                  in qt `B.cons` f `B.snoc` qt+        otherwise -> f+    in B.intercalate sep . map wrapField $ r+  +  fileHeaders _ = Nothing+++  iterCSV csvs f acc = loop acc+    where+      loop !acc' = do+        eof <- E.isEOF +        case eof of+          True -> f acc' EOF+          False -> comboIter acc'+      procRow acc' = rowParser csvs >>= f acc' . ParsedRow+      comboIter acc' = procRow acc' >>= loop+      ++  foldCSVFile fp csvs f acc = E.run iter +    where+      iter = enumFile fp $$ iterCSV csvs f acc+++  fileSink csvs fo = iter +    where+      iter :: (Maybe Handle, Int) +           -> ParsedRow Row +           -> E.Iteratee B.ByteString IO (Maybe Handle, Int)++      iter acc@(oh, i) EOF = case oh of+          Just oh' -> liftIO (hClose oh') >> yield (Nothing, i) E.EOF+          Nothing -> yield acc E.EOF++      iter acc (ParsedRow Nothing) = yield acc (E.Chunks [])++      iter (Nothing, !i) r = do+        oh <- liftIO $ openFile fo WriteMode+        iter (Just oh, i) r++      iter (Just oh, !i) (ParsedRow (Just r)) = do +        outputRowIter csvs oh r +        yield (Just oh, i+1) (E.Chunks [])+++  mapCSVFiles fis s f fo = foldM stepFile (Right 0) fis+    where+      stepFile :: (Either SomeException Int) +               -> FilePath +               -> IO (Either SomeException Int)+      stepFile res0 fi = do +        case res0 of+          Left x -> return $ Left x+          Right i -> do +            res <- foldCSVFile fi s (iter fi) (Nothing, i)+            return $ fmap snd res++      iter :: FilePath+           -> (Maybe Handle, Int) +           -> ParsedRow Row +           -> E.Iteratee B.ByteString IO (Maybe Handle, Int)+      iter fi acc@(oh, i) EOF = case oh of+        Just oh' -> liftIO (hClose oh') >> yield (Nothing, i) E.EOF+        Nothing -> yield acc E.EOF+      iter fi acc (ParsedRow Nothing) = return acc+      iter fi (Nothing, !i) (ParsedRow (Just r)) = do+        let row' = f r+        oh <- liftIO $ openFile fo AppendMode+        iter fi (Just oh, i) (ParsedRow (Just r))+      iter fi (Just oh, !i) (ParsedRow (Just r)) = do +        outputRowsIter s oh (f r) +        return (Just oh, i+1)++++------------------------------------------------------------------------------+-- 'MapRow' instance for 'CSVeable'+instance CSVeable MapRow where+  rowToStr s r = rowToStr s . M.elems $ r++  fileHeaders rs = headMay rs >>= return . M.keys++  iterCSV csvs f !acc = loop ([], acc)+    where+      loop (headers, !acc') = do+        eof <- E.isEOF +        case eof of+          True -> f acc' EOF+          False -> comboIter headers acc'++      comboIter !headers !acc' = do +        a <- procRow headers acc' +        loop (headers, a)++      -- Fill headers if not yet filled+      procRow [] !acc' = rowParser csvs >>= (\(Just hs) -> loop (hs, acc'))++      -- Process starting w/ the second row+      procRow !headers !acc' = rowParser csvs >>= +                               toMapCSV headers >>= +                               f acc' . ParsedRow ++      toMapCSV !headers !fs = yield (fs >>= (Just . M.fromList . zip headers)) (E.Chunks [])++  foldCSVFile fp csvs f !acc = E.run (enumFile fp $$ iterCSV csvs f acc)+++  fileSink s fo = mapIter+    where+      mapIter :: (Maybe Handle, Int) +              -> ParsedRow MapRow +              -> E.Iteratee B.ByteString IO (Maybe Handle, Int)+      mapIter acc@(oh, !i) EOF = case oh of+        Just oh' -> liftIO (hClose oh') >> yield (Nothing, i) E.EOF+        Nothing -> yield acc E.EOF+      mapIter !acc (ParsedRow Nothing) = return acc+      mapIter (Nothing, !i) (ParsedRow (Just (!r))) = do+        oh <- liftIO $ do+          oh' <- openFile fo WriteMode+          B.hPutStrLn oh' . rowToStr s . M.keys $ r+          return oh'+        mapIter (Just oh, i) (ParsedRow (Just r))+      mapIter (Just oh, !i) (ParsedRow (Just (!r))) = do+        outputRowIter s oh r +        return (Just oh, i+1)+++  mapCSVFiles fis s f fo = foldM stepFile (Right 0) fis+    where+      stepFile res0 fi = do +        case res0 of+          Left x -> return $ Left x+          Right i -> do +            res <- foldCSVFile fi s (iter fi) (Nothing, i)+            return $ fmap snd res++      addFileSource fi r = M.insert "FromFile" (B8.pack fi) r++      iter :: FilePath+           -> (Maybe Handle, Int) +           -> ParsedRow MapRow +           -> E.Iteratee B.ByteString IO (Maybe Handle, Int)+      iter fi acc@(oh, i) EOF = case oh of+        Just oh' -> liftIO (hClose oh') >> yield (Nothing, i) E.EOF+        Nothing -> yield acc E.EOF+      iter fi acc (ParsedRow Nothing) = return acc+      iter fi (Nothing, !i) (ParsedRow (Just r)) = do+        case f r of+          [] -> return (Nothing, i) -- the fn did not return any rows at all!+          (x:_) -> do+            oh <- liftIO $ do+              exist <- doesFileExist fo+              oh' <- openFile fo AppendMode+              case exist of+                True -> return ()+                False -> B.hPutStrLn oh' . rowToStr s . M.keys . (addFileSource fi) $ x+              return oh'+            iter fi (Just oh, i) (ParsedRow (Just r))+      iter fi (Just oh, !i) (ParsedRow (Just r)) = +        let rows = f . addFileSource fi $ r+        in do+          outputRowsIter s oh rows +          return (Just oh, i+1)+++------------------------------------------------------------------------------+readCSVFile :: (CSVeable r) => CSVSettings  -- ^ CSV settings+            -> FilePath   -- ^ FilePath+            -> IO (Either SomeException [r])  -- ^ Collected data+readCSVFile s fp = do+  res <- foldCSVFile fp s collectRows []+  return $ case res of+    Left e -> Left e+    Right rs -> Right (reverse rs)+++------------------------------------------------------------------------------+writeCSVFile :: (CSVeable r) => CSVSettings   -- ^ CSV settings+             -> FilePath  -- ^ Target file path+             -> [r]   -- ^ Data to be output+             -> IO Int  -- ^ Number of rows written+writeCSVFile s fp rs = +  let doOutput h = writeHeaders s h rs >> outputRowsIter h+      outputRowsIter h = foldM (step h) 0  . map (rowToStr s) $ rs+      step h acc x = (B.hPutStrLn h x) >> return (acc+1)+  in bracket+      (openFile fp WriteMode)+      (hClose)+      (doOutput)++------------------------------------------------------------------------------+appendCSVFile :: (CSVeable r) => CSVSettings   -- ^ CSV settings+             -> FilePath  -- ^ Target file path+             -> [r]   -- ^ Data to be output+             -> IO Int  -- ^ Number of rows written+appendCSVFile s fp rs = +  let doOutput (c,h) = when c (writeHeaders s h rs) >> outputRowsIter h+      outputRowsIter h = foldM (step h) 0  . map (rowToStr s) $ rs+      step h acc x = (B.hPutStrLn h x) >> return (acc+1)+      chkOpen = do+        writeHeaders <- do+          fe <- doesFileExist fp +          if fe+          	then do+          	  fs <- getFileStatus fp >>= return . fileSize+          	  return $ if fs > 0 then False else True+            else return True+        h <- openFile fp AppendMode+        return (writeHeaders, h)+  in bracket+      (chkOpen)+      (hClose . snd)+      (doOutput)++------------------------------------------------------------------------------+-- | Output given row into given handle+outputRow :: CSVeable r => CSVSettings -> Handle -> r -> IO ()+outputRow s oh = B.hPutStrLn oh . rowToStr s+++outputRows :: CSVeable r => CSVSettings -> Handle -> [r] -> IO ()+outputRows s oh = mapM_ (outputRow s oh)+++writeHeaders :: CSVeable r => CSVSettings -> Handle -> [r] -> IO ()+writeHeaders s h rs = case fileHeaders rs of+  Just hs -> B.hPutStrLn h . rowToStr s $ hs+  Nothing -> return ()+++outputRowIter :: CSVeable r => CSVSettings -> Handle -> r -> E.Iteratee B.ByteString IO ()+outputRowIter s oh = liftIO . outputRow s oh+++outputRowsIter :: CSVeable r => CSVSettings -> Handle -> [r] -> E.Iteratee B.ByteString IO ()+outputRowsIter s oh rs = mapM_ (outputRowIter s oh) rs+++------------------------------------------------------------------------------+-- | A datatype that incorporates the signaling of parsing status to the+--user-developed iteratee.+--+-- We need this because some iteratees do interleaved IO (such as outputting to+-- a file via a handle inside the accumulator) and some final actions may need+-- to be taken upon encountering EOF (such as closing the interleaved handle).+--+-- Use this datatype when developing iteratees for use with fold* family of+-- functions (Row enumarators).+data (CSVeable r) => ParsedRow r = ParsedRow (Maybe r) | EOF++------------------------------------------------------------------------------+-- | An iteratee that processes each row of a CSV file and updates the+-- accumulator.+--+-- You would implement one of these to use with the 'foldCSVFile' function.+type CSVAction r a = a -> ParsedRow r -> E.Iteratee B.ByteString IO a+++------------------------------------------------------------------------------+-- | Convenience converter for fold step functions that live in the IO monad.+--+-- Use this if you don't want to deal with Iteratees when writing your fold+-- functions.+funToIterIO :: (CSVeable r) => (a -> ParsedRow r -> IO a) -> CSVAction r a+funToIterIO f = iterf+  where+    iterf !acc EOF = liftIO (f acc EOF) >>= \(!acc') -> yield acc' E.EOF+    iterf !acc r = liftIO (f acc r) >>= \(!acc') -> yield acc' (E.Chunks [])+++------------------------------------------------------------------------------+-- | Convenience converter for fold step functions that are pure.+--+-- Use this if you don't want to deal with Iteratees when writing your fold+-- functions.+funToIter :: (CSVeable r) => (a -> ParsedRow r -> a) -> CSVAction r a+funToIter f = iterf+  where+    iterf !acc EOF = yield (f acc EOF) E.EOF+    iterf !acc r = yield (f acc r) (E.Chunks [])+++------------------------------------------------------------------------------+-- | Just collect all rows into an array. This will cancel out the incremental+-- nature of this library.+collectRows :: CSVeable r => CSVAction r [r]+collectRows acc EOF = yield acc (E.Chunks [])+collectRows acc (ParsedRow (Just r)) = let a' = (r:acc) +                                       in a' `seq` yield a' (E.Chunks [])+collectRows acc (ParsedRow Nothing) = yield acc (E.Chunks [])++-- * Parsers++rowParser :: (Monad m, MonadIO m) => CSVSettings -> E.Iteratee B.ByteString m (Maybe Row)+rowParser csvs = E.catchError p handler +  where +    p = iterParser $ row csvs+    handler e = do+      liftIO $ putStrLn ("Error in parsing: " ++ show e)+      yield Nothing (E.Chunks [])+      +row :: CSVSettings -> Parser (Maybe Row)+row csvs = csvrow csvs <|> badrow++badrow :: Parser (Maybe Row)+badrow = P.takeWhile (not . C8.isEndOfLine) *> +         (C8.endOfLine <|> C8.endOfInput) *> return Nothing++csvrow :: CSVSettings -> Parser (Maybe Row)+csvrow c = +  let !rowbody = (quotedField' <|> (field c)) `sepBy` C8.char (csvSep c)+      !properrow = rowbody <* (C8.endOfLine <|> P.endOfInput)+      quotedField' = case csvQuoteChar c of+          Nothing -> mzero+          Just q' -> try (quotedField q')+  in do+    res <- properrow+    return $ Just res++field :: CSVSettings -> Parser Field+field s = P.takeWhile (isFieldChar s) <?> "Parsing a regular field"++isFieldChar s = notInClass xs'+  where xs = csvSep s : "\n\r"+        xs' = case csvQuoteChar s of +          Nothing -> xs+          Just x -> x : xs++quotedField :: Char -> Parser Field+quotedField c = let w = c2w c in do+  (C8.char c) <?> "Quote start"+  f <- many (notWord8 w <|> (string (B.pack $ [w,w]) *> return w))+  (C8.char c) <?> "Quote end"+  return $ B.pack f++
+ src/Data/CSV/Enumerator/Types.hs view
@@ -0,0 +1,46 @@+{-# LANGUAGE OverloadedStrings, BangPatterns #-}+{-# LANGUAGE PackageImports #-}+{-# LANGUAGE TypeSynonymInstances #-}+++module Data.CSV.Enumerator.Types where+++import qualified Data.ByteString as B+import qualified Data.Map as M++-- | Settings for a CSV file. This library is intended to be flexible and offer a way to process the majority of text data files out there.+data CSVSettings = CSVS+  { +    -- | Separator character to be used in between fields+    csvSep :: Char          ++    -- | Quote character that may sometimes be present around fields. If 'Nothing' is given, the library will never expect quotation even if it is present.+  , csvQuoteChar :: Maybe Char+  +    -- | Quote character that should be used in the output.+  , csvOutputQuoteChar :: Maybe Char+  +    -- | Field separator that should be used in the output.+  , csvOutputColSep :: Char+  } deriving (Read, Show, Eq)++-- | Default settings for a CSV file. +--+-- > csvSep = ','+-- > csvQuoteChar = Just '"'+-- > csvOutputQuoteChar = Just '"'+-- > csvOutputColSep = ','+--+defCSVSettings :: CSVSettings+defCSVSettings = CSVS+  { csvSep = ','+  , csvQuoteChar = Just '"'+  , csvOutputQuoteChar = Just '"'+  , csvOutputColSep = ','+  } ++type Row = [Field]+type Field = B.ByteString+type MapRow = M.Map B.ByteString B.ByteString+