packages feed

hadoop-streaming 0.1.0.0 → 0.2.0.0

raw patch · 7 files changed

+356/−100 lines, 7 filesdep +bytestringdep ~textPVP ok

version bump matches the API change (PVP)

Dependencies added: bytestring

Dependency ranges changed: text

API changes (from Hackage documentation)

- HadoopStreaming: runMapperWith :: MonadIO m => ConduitT () Text m () -> ConduitT Text Void m () -> (Text -> e -> m ()) -> Mapper e m -> m ()
- HadoopStreaming: runReducerWith :: MonadIO m => ConduitT () Text m () -> ConduitT Text Void m () -> (Text -> e -> m ()) -> Reducer e m -> m ()
- HadoopStreaming: sinkHandle :: MonadIO m => Handle -> ConduitT Text o m ()
- HadoopStreaming: sourceHandle :: MonadIO m => Handle -> ConduitT i Text m ()
+ HadoopStreaming.ByteString: defaultKeyValueDecoder :: (ByteString -> Either e k) -> (ByteString -> Either e v) -> ByteString -> Either e (k, Maybe v)
+ HadoopStreaming.ByteString: defaultKeyValueEncoder :: (k -> ByteString) -> (v -> ByteString) -> k -> v -> ByteString
+ HadoopStreaming.ByteString: sinkHandle :: MonadIO m => Handle -> ConduitT ByteString o m ()
+ HadoopStreaming.ByteString: sourceHandle :: MonadIO m => Handle -> ConduitT i ByteString m ()
+ HadoopStreaming.ByteString: stdinLn :: (MonadIO m, MonadThrow m) => ConduitT i ByteString m ()
+ HadoopStreaming.ByteString: stdoutLn :: MonadIO m => ConduitT ByteString o m ()
+ HadoopStreaming.Text: defaultKeyValueDecoder :: (Text -> Either e k) -> (Text -> Either e v) -> Text -> Either e (k, Maybe v)
+ HadoopStreaming.Text: defaultKeyValueEncoder :: (k -> Text) -> (v -> Text) -> k -> v -> Text
+ HadoopStreaming.Text: sinkHandle :: MonadIO m => Handle -> ConduitT Text o m ()
+ HadoopStreaming.Text: sourceHandle :: MonadIO m => (IOException -> m ()) -> Handle -> ConduitT i Text m ()
+ HadoopStreaming.Text: stdinLn :: (MonadIO m, MonadThrow m) => ConduitT i Text m ()
+ HadoopStreaming.Text: stdoutLn :: MonadIO m => ConduitT Text o m ()
- HadoopStreaming: Mapper :: (Text -> Either e input) -> (k -> v -> Text) -> ConduitT input (k, v) m () -> Mapper e m
+ HadoopStreaming: Mapper :: (i -> Either e j) -> (k -> v -> o) -> ConduitT j (k, v) m () -> Mapper i o e m
- HadoopStreaming: Reducer :: (Text -> Either e (k, v)) -> (res -> Text) -> (k -> v -> ConduitT v res m ()) -> Reducer e m
+ HadoopStreaming: Reducer :: (i -> Either e (k, v)) -> (r -> o) -> (k -> v -> ConduitT v r m ()) -> Reducer i o e m
- HadoopStreaming: data Mapper e m
+ HadoopStreaming: data Mapper i o e m
- HadoopStreaming: data Reducer e m
+ HadoopStreaming: data Reducer i o e m
- HadoopStreaming: runMapper :: MonadIO m => (Text -> e -> m ()) -> Mapper e m -> m ()
+ HadoopStreaming: runMapper :: MonadIO m => ConduitT () i m () -> ConduitT o Void m () -> (i -> e -> m ()) -> Mapper i o e m -> m ()
- HadoopStreaming: runReducer :: MonadIO m => (Text -> e -> m ()) -> Reducer e m -> m ()
+ HadoopStreaming: runReducer :: MonadIO m => ConduitT () i m () -> ConduitT o Void m () -> (i -> e -> m ()) -> Reducer i o e m -> m ()

Files

CHANGELOG.md view
@@ -1,5 +1,9 @@ # Revision history for hadoop-streaming +## 0.2.0.0 -- 2020-04-05++* Make input and output types abstract.+ ## 0.1.0.0 -- 2020-04-01  * First version. Released on an unsuspecting world.
README.md view
@@ -1,3 +1,46 @@ A simple Hadoop streaming library based on [conduit](https://hackage.haskell.org/package/conduit), useful for writing mapper and reducer logic in Haskell and running it on AWS Elastic MapReduce, Azure HDInsight, GCP Dataproc, and so forth.++Hackage: https://hackage.haskell.org/package/hadoop-streaming++# Word Count Example++See the [Haddock in `HadoopStreaming.Text`](https://hackage.haskell.org/package/hadoop-streaming-0.1.0.0/docs/HadoopStreaming-Text.html)+for a simple word-count example.++# A Few Things to Note++## ByteString vs Text++The `HadoopStreaming` module provides the general `Mapper` and `Reducer` data types, whose input and output+types are abstract. They are usually instantiated with either `ByteString` or `Text`.+`ByteString` is more suitable if the input/output needs to be decoded/encoded, for instance using the+`base64-bytestring` library. On the other hand, `Text` could make more sense if decoding/encoding is not needed,+or if the data is not UTF-8 encoded (see below regarding encodings). In general I'd imagine `ByteString` being+used much more often than `Text`.++The `HadoopStreaming.ByteString` and `HadoopStreaming.Text` modules provide some utilities for working with+`ByteString` and `Text`, respectively.++## Encoding++It is highly recommended that your input data be UTF-8 encoded, as this is the default encoding Hadoop uses. If you+must use other encodings such as UTF-16, keep in mind the following gotchas:++- It is not enough that your code can work with the encoding you choose to use:++  - By default, if any of your input files does not end with a UTF-8 representation of newline, i.e., a `0x0A` byte, Hadoop+    streaming will add a `0x0A` byte.++  - Likewise, if any line in your mapper output does not contain a UTF-8 representation of tab (`0x09`), Hadoop streaming will+    add it at the end of the line.++  This will almost certainly break your job. It may be possible to configure Hadoop streaming and tell it to use other encodings,+  so that the above behavior is consistent with the encoding you choose to use, but I don't know whether that is the case. I tried+  `-D mapreduce.map.java.opts="-Dfile.encoding=UTF-16BE"` but that doesn't seem to work.++- If you use `ByteString` as the input type and use `Data.ByteString.hGetLine` to read lines from the input, be+  aware that `Data.ByteString.hGetLine` uses `0x0A` bytes as line breaks, so it doesn't work properly for+  non-UTF-8 encoded input. For example, in UTF-16BE and UTF-16LE, the newline character is encoded as `0x00 0x0A` and `0x0A 0x00`,+  respectively.
hadoop-streaming.cabal view
@@ -1,7 +1,7 @@ cabal-version:       2.4  name:                hadoop-streaming-version:             0.1.0.0+version:             0.2.0.0 synopsis:            A simple Hadoop streaming library description:   A simple Hadoop streaming library based on <https://hackage.haskell.org/package/conduit conduit>,@@ -29,6 +29,8 @@ library   exposed-modules:       HadoopStreaming+      HadoopStreaming.ByteString+      HadoopStreaming.Text   other-modules:       Paths_hadoop_streaming   autogen-modules:@@ -37,6 +39,7 @@       src   build-depends:       base >=4.12 && <5,+      bytestring >=0.10 && <0.11,       conduit >= 1.3.1 && <1.4,       extra >= 1.6.18 && <1.8,       text >=1.2.4.0 && <1.3@@ -52,10 +55,10 @@   ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N   build-depends:       base,+      bytestring,       conduit,       extra,       hspec,-      text,       hadoop-streaming   default-language: Haskell2010   build-tool-depends: hspec-discover:hspec-discover == 2.*
src/HadoopStreaming.hs view
@@ -2,23 +2,25 @@ {-# LANGUAGE GADTs #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-} +-----------------------------------------------------------------------------+-- |+-- Module      :  HadoopStreaming+-- Maintainer  :  Ziyang Liu <free@cofree.io>+--+-- Hadoop streaming makes it possible to write Hadoop jobs in Haskell. See the+-- <https://hadoop.apache.org/docs/current/hadoop-streaming/HadoopStreaming.html official documentation> for how+-- Hadoop streaming works. See "HadoopStreaming.Text" for a word count example. module HadoopStreaming   ( Mapper(..)   , Reducer(..)   , runMapper-  , runMapperWith   , runReducer-  , runReducerWith   , println   , incCounter   , incCounterBy-  , sourceHandle-  , sinkHandle   ) where -import           Control.Monad.Extra (unlessM) import           Control.Monad.IO.Class (MonadIO(..)) import           Data.Conduit (ConduitT, runConduit, (.|)) import qualified Data.Conduit as C@@ -30,24 +32,24 @@ import qualified System.IO as IO  --- | A @Mapper@ consists of a decoder, an encoder, and a stream that transforms--- each input into a @(key, value)@ pair.-data Mapper e m = forall input k v. Mapper-  (Text -> Either e input)-  -- ^ Decoder for mapper input.-  (k -> v -> Text)-  -- ^ Encoder for mapper output.-  (ConduitT input (k, v) m ())+-- | A @Mapper@ consists of a decoder, an encoder, and a stream transforming+-- input into @(key, value)@ pairs.+data Mapper i o e m = forall j k v. Mapper+  (i -> Either e j)+  -- ^ Decoder for mapper input+  (k -> v -> o)+  -- ^ Encoder for mapper output+  (ConduitT j (k, v) m ())   -- ^ A stream transforming @input@ into @(k, v)@ pairs. --- | A @Reducer@ consists of a decoder, an encoder, and a stream that transforms--- each key and all values associated with the key into zero or more @res@.-data Reducer e m = forall k v res. Eq k => Reducer-  (Text -> Either e (k, v))+-- | A @Reducer@ consists of a decoder, an encoder, and a stream transforming+-- each key and all values associated with the key into some result values.+data Reducer i o e m = forall k v r. Eq k => Reducer+  (i -> Either e (k, v))   -- ^ Decoder for reducer input-  (res -> Text)+  (r -> o)   -- ^ Encoder for reducer output-  (k -> v -> ConduitT v res m ())+  (k -> v -> ConduitT v r m ())   -- ^ A stream processing a key and all values associated with the key. The parameter   -- @v@ is the first value associated with the key (since a key always has one or more   -- values), and the remaining values are processed by the conduit.@@ -68,83 +70,59 @@   --   incCounter "reducer" "key-value pairs" >> pure (k, v)   -- @ --- | Run a 'Mapper'. Takes input from 'stdin' and emits the result to 'stdout'.------ > runMapper = runMapperWith (sourceHandle stdin) (sinkHandle stdout)++-- | Run a 'Mapper'. runMapper   :: MonadIO m-  => (Text -> e -> m ())-  -- ^ A action to be executed for each input that cannot be decoded. The first parameter+  => ConduitT () i m ()+  -- ^ Mapper source. The source should generally stream from @stdin@, and produce+  -- a value for each line of the input, as that is how Hadoop streaming is supposed+  -- to work, but this is not enforced. For example, if you really want to, you can produce a single+  -- value for the entire input split (which can be done using @Data.Conduit.Combinators.stdin@+  -- as the source). Note that regardless of what the source does, the values of Hadoop counters+  -- "Map input records" and "Reduce input records" are always the number of lines of the mapper and reducer+  -- input, because these counters are managed by the Hadoop framework.+  --+  -- An example is 'HadoopStreaming.Text.stdinLn', which streams from 'IO.stdin' as 'Text', one line+  -- at a time.+  -> ConduitT o C.Void m ()+  -- ^ Mapper sink. It should generally stream to 'IO.stdout'. An example is+  -- 'HadoopStreaming.Text.stdoutLn'.+  -> (i -> e -> m ())+  -- ^ An action to be executed for each input that cannot be decoded. The first parameter   -- is the input and the second parameter is the decoding error. One may choose to, for instance,   -- increment a counter and 'println' an error message.-  -> Mapper e m -> m ()-runMapper = runMapperWith stdin stdout---- | Like 'runMapper', but allows specifying a source and a sink.------ > runMapper = runMapperWith (sourceHandle stdin) (sinkHandle stdout)-runMapperWith-  :: MonadIO m-  => ConduitT () Text m ()-  -> ConduitT Text C.Void m ()-  -> (Text -> e -> m ())-  -> Mapper e m -> m ()-runMapperWith source sink f (Mapper dec enc trans) = runConduit $+  -> Mapper i o e m -> m ()+runMapper source sink f (Mapper dec enc trans) = runConduit $     source .| CL.mapMaybeM g .| trans .| C.map (uncurry enc) .| sink   where     g input = either ((Nothing <$) . f input) (pure . Just) (dec input) --- | Run a 'Reducer'. Takes input from 'stdin' and emits the result to 'stdout'.------ > runReducer = runReducerWith (sourceHandle stdin) (sinkHandle stdout)+-- | Run a 'Reducer'. runReducer   :: MonadIO m-  => (Text -> e -> m ())-  -- ^ A action to be executed for each input that cannot be decoded. The first parameter-  -- is the input and the second parameter is the decoding error. One may choose to, for instance,-  -- increment a counter and 'println' an error message.-  -> Reducer e m -> m ()-runReducer = runReducerWith stdin stdout---- | Like 'runReducer', but allows specifying a source and a sink.------ > runReducer = runReducerWith (sourceHandle stdin) (sinkHandle stdout)-runReducerWith-  :: MonadIO m-  => ConduitT () Text m ()-  -> ConduitT Text C.Void m ()-  -> (Text -> e -> m ())-  -> Reducer e m -> m ()-runReducerWith source sink f (Reducer dec enc trans) = runConduit $+  => ConduitT () i m ()+  -- ^ Reducer source+  -> ConduitT o C.Void m ()+  -- ^ Reducer sink+  -> (i -> e -> m ())+  -- ^ An action to be executed for each input that cannot be decoded.+  -> Reducer i o e m -> m ()+runReducer source sink f (Reducer dec enc trans) = runConduit $     source .| CL.mapMaybeM g .| CL.groupOn1 fst .| reduceKey trans .| C.map enc .| sink   where     g input = either ((Nothing <$) . f input) (pure . Just) (dec input) -reduceKey :: forall m k v res. Monad m-          => (k -> v -> ConduitT v res m ())-          -> ConduitT ((k,v), [(k,v)]) res m ()+reduceKey+  :: Monad m+  => (k -> v -> ConduitT v r m ())+  -> ConduitT ((k,v), [(k,v)]) r m () reduceKey f = go   where     go = C.await >>= maybe (pure ()) \((k, v), kvs) ->       C.yieldMany (map snd kvs) .| f k v >> go -stdin :: MonadIO m => ConduitT i Text m ()-stdin = sourceHandle IO.stdin--stdout :: MonadIO m => ConduitT Text o m ()-stdout = sinkHandle IO.stdout---- | Stream the contents of a 'IO.Handle' one line at a time as 'Text'.-sourceHandle :: MonadIO m => IO.Handle -> ConduitT i Text m ()-sourceHandle h = go .| C.filter (not . Text.all (== ' '))-  where-    go = unlessM (liftIO (IO.hIsEOF h)) (liftIO (Text.hGetLine h) >>= C.yield >> go)---- | Stream data to a 'IO.Handle', separated by @\\n@.-sinkHandle :: MonadIO m => IO.Handle -> ConduitT Text o m ()-sinkHandle h = C.awaitForever (liftIO . Text.hPutStrLn h)---- | Like 'Text.putStrLn', but writes to 'stderr'.+-- | Like 'Text.putStrLn', but writes to 'IO.stderr'. println :: MonadIO m => Text -> m () println = liftIO . Text.hPutStrLn IO.stderr 
+ src/HadoopStreaming/ByteString.hs view
@@ -0,0 +1,93 @@+{-# LANGUAGE BlockArguments #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE TupleSections #-}++-----------------------------------------------------------------------------+-- |+-- Module      :  HadoopStreaming.ByteString+-- Maintainer  :  Ziyang Liu <free@cofree.io>+--+-- This module has some utilities for working with 'ByteString' in Hadoop streaming.+module HadoopStreaming.ByteString+  ( sourceHandle+  , sinkHandle+  , stdinLn+  , stdoutLn+  , defaultKeyValueEncoder+  , defaultKeyValueDecoder+  ) where++import           Conduit (MonadThrow(..), lift)+import           Control.Exception (IOException, try)+import           Control.Monad.Extra (unlessM, (>=>))+import           Control.Monad.IO.Class (MonadIO(..))+import           Data.Conduit (ConduitT, (.|))+import qualified Data.Conduit as C+import qualified Data.Conduit.Combinators as C+import           Data.ByteString (ByteString)+import qualified Data.ByteString.Char8 as BC+import qualified System.IO as IO+++-- | Stream the contents of a 'IO.Handle' one line at a time as 'ByteString'.+--+-- __NB__: This only works if the input from the 'IO.Handle' is UTF-8 encoded.+sourceHandle+  :: MonadIO m => IO.Handle -> ConduitT i ByteString m ()+sourceHandle h = go .| C.filter (not . BC.all (== ' '))+  where+    go = unlessM (liftIO (IO.hIsEOF h)) (liftIO (BC.hGetLine h) >>= C.yield >> go)++-- | Stream data to a 'IO.Handle', separated by @\\n@.+--+-- __NB__: This only works if the data is UTF-8 encoded.+sinkHandle :: MonadIO m => IO.Handle -> ConduitT ByteString o m ()+sinkHandle h = C.awaitForever (liftIO . BC.hPutStrLn h)++-- | Stream the contents from 'System.IO.stdin' one line at a time as 'ByteString'.+--+-- __NB__: This only works if the input from the 'IO.Handle' is UTF-8 encoded.+--+-- > stdinLn = sourceHandle System.IO.stdin+stdinLn :: (MonadIO m, MonadThrow m) => ConduitT i ByteString m ()+stdinLn = sourceHandle IO.stdin++-- | Stream data to 'System.IO.stdout', separated by @\\n@.+--+-- __NB__: This only works if the data is UTF-8 encoded.+--+-- > stdoutLn = sinkHandle System.IO.stdout+stdoutLn :: MonadIO m => ConduitT ByteString o m ()+stdoutLn = sinkHandle IO.stdout++-- | Encode a key-value pair by separating them with a (UTF-8 encoded) tab (i.e., a @0x09@ byte),+-- which is the default way the mapper output should be formatted.+defaultKeyValueEncoder+  :: (k -> ByteString)+  -- ^ Key encoder+  -> (v -> ByteString)+  -- ^ Value encoder+  -> k -> v -> ByteString+defaultKeyValueEncoder encKey encValue k v = encKey k <> "\t" <> encValue v++-- | Decode a line by treating the prefix up to the first tab as key, and the suffix after the first+-- tab as value. If the line does not contain a tab, or if the first tab is the last character,+-- the whole line is considered as key, and the value decoder is not used.+--+-- __NB__: This only works if the data is UTF-8 encoded.+defaultKeyValueDecoder+  :: (ByteString -> Either e k)+  -- ^ Key decoder+  -> (ByteString -> Either e v)+  -- ^ Value decoder+  -> ByteString -> Either e (k, Maybe v)+defaultKeyValueDecoder decKey decValue i+    | BC.length i2 <= 1 = (,Nothing) <$> decKey i1+    | otherwise = do+        k <- decKey i1+        v <- decValue (BC.tail i2)+        pure (k, Just v)+  where+    (i1, i2) = BC.break (== '\t') i
+ src/HadoopStreaming/Text.hs view
@@ -0,0 +1,134 @@+{-# LANGUAGE BlockArguments #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE TypeApplications #-}++-----------------------------------------------------------------------------+-- |+-- Module      :  HadoopStreaming.Text+-- Maintainer  :  Ziyang Liu <free@cofree.io>+--+-- This module has some utilities for working with 'Text' in Hadoop streaming.+--+-- Word count example:+--+-- @+--  {-\# LANGUAGE OverloadedStrings, TupleSections \#-}+--+--  import Data.Conduit (ConduitT)+--  import qualified Data.Conduit as C+--  import qualified Data.Conduit.Combinators as C+--  import Data.Void (Void)+--  import HadoopStreaming+--  import qualified HadoopStreaming.Text as HT+--  import Data.Text (Text)+--  import qualified Data.Text as Text+--+--  mapper :: Mapper Text Text Void IO+--  mapper = Mapper dec enc trans+--    where+--      dec :: Text -> Either Void [Text]+--      dec = Right . Text.words+--+--      enc :: Text -> Int -> Text+--      enc = HT.defaultKeyValueEncoder id (Text.pack . show)+--+--      trans :: ConduitT [Text] (Text, Int) IO ()+--      trans = C.concatMap (map (,1))+--+--  reducer :: Reducer Text Text Void IO+--  reducer = Reducer dec enc trans+--    where+--      dec :: Text -> Either Void (Text, Int)+--      dec i = Right (i1, read . tail . Text.unpack $ i2)+--        where (i1, i2) = Text.break (== '\\t') i+--+--      enc :: (Text, Int) -> Text+--      enc (t, c) = t <> "," <> Text.pack (show c)+--+--      trans :: Text -> Int -> ConduitT Int (Text, Int) IO ()+--      trans k v0 = C.foldl (+) v0 >>= C.yield . (k,)+-- @+module HadoopStreaming.Text+  ( sourceHandle+  , sinkHandle+  , stdinLn+  , stdoutLn+  , defaultKeyValueEncoder+  , defaultKeyValueDecoder+  ) where++import           Conduit (MonadThrow(..), lift)+import           Control.Exception (IOException, try)+import           Control.Monad.Extra (unlessM, (>=>))+import           Control.Monad.IO.Class (MonadIO(..))+import           Data.Conduit (ConduitT, (.|))+import qualified Data.Conduit as C+import qualified Data.Conduit.Combinators as C+import           Data.Text (Text)+import qualified Data.Text as Text+import qualified Data.Text.IO as Text+import qualified System.IO as IO+++-- | Stream the contents of a 'IO.Handle' one line at a time as 'Text'.+sourceHandle+  :: MonadIO m+  => (IOException -> m ())+  -- ^ An action to be executed if there is an error reading the input. This is usually+  -- caused by the input having an incorrect encoding or containing corrupt data.+  -- The recommended action is to log an error message and fail the job.+  --+  -- __NB__: The stream will terminate if an error occurrs, regardless of whether this+  -- action re-throws the error or not.+  -> IO.Handle -> ConduitT i Text m ()+sourceHandle f h = go .| C.filter (not . Text.all (== ' '))+  where+    go = unlessM (liftIO (IO.hIsEOF h)) $+      liftIO (try @IOException (Text.hGetLine h)) >>= either (lift . f) (C.yield >=> const go)++-- | Stream data to a 'IO.Handle', separated by @\\n@.+sinkHandle :: MonadIO m => IO.Handle -> ConduitT Text o m ()+sinkHandle h = C.awaitForever (liftIO . Text.hPutStrLn h)++-- | Stream the contents from 'System.IO.stdin' one line at a time as 'Text'.+--+-- > stdinLn = sourceHandle throwM System.IO.stdin+stdinLn :: (MonadIO m, MonadThrow m) => ConduitT i Text m ()+stdinLn = sourceHandle throwM IO.stdin++-- | Stream data to 'System.IO.stdout', separated by @\\n@.+--+-- > stdoutLn = sinkHandle System.IO.stdout+stdoutLn :: MonadIO m => ConduitT Text o m ()+stdoutLn = sinkHandle IO.stdout++-- | Encode a key-value pair by separating them with a tab, which is the default way+-- the mapper output should be formatted.+defaultKeyValueEncoder+  :: (k -> Text)+  -- ^ Key encoder+  -> (v -> Text)+  -- ^ Value encoder+  -> k -> v -> Text+defaultKeyValueEncoder encKey encValue k v = encKey k <> "\t" <> encValue v++-- | Decode a line by treating the prefix up to the first tab as key, and the suffix after the first+-- tab as value. If the line does not contain a tab, or if the first tab is the last character,+-- the whole line is considered as key, and the value decoder is not used.+defaultKeyValueDecoder+  :: (Text -> Either e k)+  -- ^ Key decoder+  -> (Text -> Either e v)+  -- ^ Value decoder+  -> Text -> Either e (k, Maybe v)+defaultKeyValueDecoder decKey decValue i+    | Text.length i2 <= 1 = (,Nothing) <$> decKey i1+    | otherwise = do+        k <- decKey i1+        v <- decValue (Text.tail i2)+        pure (k, Just v)+  where+    (i1, i2) = Text.break (== '\t') i
test/hspec/HadoopStreamingSpec.hs view
@@ -4,13 +4,14 @@  import qualified Data.Conduit as C import qualified Data.Conduit.Combinators as C-import Data.Text (Text)-import qualified Data.Text as Text-import System.IO.Extra+import           Data.ByteString (ByteString)+import qualified Data.ByteString.Char8 as B+import           System.IO.Extra (IOMode(..), withFile, withTempFile) -import HadoopStreaming+import           HadoopStreaming+import           HadoopStreaming.ByteString -import Test.Hspec+import           Test.Hspec   ignoreDecodeError :: a -> b -> IO ()@@ -27,14 +28,14 @@           withFile temp WriteMode $ \hout -> do             let mapper = Mapper dec enc trans                   where-                    dec :: Text -> Either String (Int, Int)-                    dec = Right . read . Text.unpack+                    dec :: ByteString -> Either String (Int, Int)+                    dec = Right . read . B.unpack -                    enc :: Int -> Int -> Text-                    enc x y = Text.pack $ show x ++ ", " ++ show y+                    enc :: Int -> Int -> ByteString+                    enc x y = B.pack $ show x ++ ", " ++ show y                      trans = C.map $ \(x, y) -> (x + 1000, y + 100)-            runMapperWith (sourceHandle hin) (sinkHandle hout) ignoreDecodeError mapper+            runMapper (sourceHandle hin) (sinkHandle hout) ignoreDecodeError mapper           readFile temp       expected <- readFile fout @@ -48,14 +49,14 @@           withFile temp WriteMode $ \hout -> do             let reducer = Reducer dec enc trans                   where-                    dec :: Text -> Either String (Int, Int)-                    dec = Right . read . Text.unpack+                    dec :: ByteString -> Either String (Int, Int)+                    dec = Right . read . B.unpack -                    enc :: (Int, Int) -> Text-                    enc (x, y) = Text.pack $ show x ++ ", " ++ show y+                    enc :: (Int, Int) -> ByteString+                    enc (x, y) = B.pack $ show x ++ ", " ++ show y                      trans = \k v -> C.foldl (+) v >>= C.yield . (k,)-            runReducerWith (sourceHandle hin) (sinkHandle hout) ignoreDecodeError reducer+            runReducer (sourceHandle hin) (sinkHandle hout) ignoreDecodeError reducer           readFile temp       expected <- readFile fout @@ -69,16 +70,16 @@           withFile temp WriteMode $ \hout -> do             let mapper = Mapper dec enc trans                   where-                    dec :: Text -> Either () (Int, Int)-                    dec (read . Text.unpack -> (x, y))+                    dec :: ByteString -> Either () (Int, Int)+                    dec (read . B.unpack -> (x, y))                       | odd x = Right (x, y)                       | otherwise = Left () -                    enc :: Int -> Int -> Text-                    enc x y = Text.pack $ show x ++ ", " ++ show y+                    enc :: Int -> Int -> ByteString+                    enc x y = B.pack $ show x ++ ", " ++ show y                      trans = C.map $ \(x, y) -> (x + 1000, y + 100)-            runMapperWith (sourceHandle hin) (sinkHandle hout) ignoreDecodeError mapper+            runMapper (sourceHandle hin) (sinkHandle hout) ignoreDecodeError mapper           readFile temp       expected <- readFile fout