diff --git a/enumerator.cabal b/enumerator.cabal
--- a/enumerator.cabal
+++ b/enumerator.cabal
@@ -1,5 +1,5 @@
 name: enumerator
-version: 0.4.8
+version: 0.4.9
 synopsis: Reliable, high-performance processing with left-fold enumerators
 license: MIT
 license-file: license.txt
diff --git a/hs/Data/Enumerator.hs b/hs/Data/Enumerator.hs
--- a/hs/Data/Enumerator.hs
+++ b/hs/Data/Enumerator.hs
@@ -39,6 +39,8 @@
 	, ($$)
 	, (>==>)
 	, (<==<)
+	, (=$)
+	, ($=)
 	
 	-- ** Running iteratees
 	, run
@@ -54,9 +56,12 @@
 	, joinE
 	, Data.Enumerator.sequence
 	, enumEOF
+	, checkContinue0
+	, checkContinue1
 	, checkDoneEx
 	, checkDone
 	, isEOF
+	, tryIO
 	
 	-- ** Testing and debugging
 	, printChunks
@@ -451,7 +456,36 @@
 	check (Yield x _) = return x
 	check (Error e) = throwError e
 
+infixr 0 =$
 
+
+-- | @enum =$ iter = 'joinI' (enum $$ iter)
+--
+-- &#x201c;Wraps&#x201d; an iteratee /inner/ in an enumeratee /wrapper/.
+-- The resulting iteratee will consume /wrapper/&#x2019;s input type and
+-- yield /inner/&#x2019;s output type.
+--
+-- Note: if the inner iteratee yields leftover input when it finishes,
+-- that extra will be discarded.
+--
+-- As an example, consider an iteratee that converts a stream of UTF8-encoded
+-- bytes into a single 'TL.Text':
+--
+-- > consumeUTF8 :: Monad m => Iteratee ByteString m Text
+--
+-- It could be written with either 'joinI' or '(=$)':
+--
+-- > import Data.Enumerator.Text as ET
+-- >
+-- > consumeUTF8 = joinI (decode utf8 $$ ET.consume)
+-- > consumeUTF8 = decode utf8 =$ ET.consume
+--
+-- Since: 0.4.9
+
+(=$) :: Monad m => Enumeratee ao ai m b -> Iteratee ai m b -> Iteratee ao m b
+enum =$ iter = joinI (enum $$ iter)
+
+
 -- | Flatten an enumerator/enumeratee pair into a single enumerator.
 
 joinE :: Monad m
@@ -465,7 +499,37 @@
 		Yield x _ -> return x
 		Continue _ -> error "joinE: divergent iteratee"
 
+infixr 0 $=
 
+
+-- | @enum $= enee = 'joinE' enum enee@
+--
+-- &#x201c;Wraps&#x201d; an enumerator /inner/ in an enumeratee /wrapper/.
+-- The resulting enumerator will generate /wrapper/&#x2019;s output type.
+--
+-- As an example, consider an enumerator that yields line character counts
+-- for a text file (e.g. for source code readability checking):
+--
+-- > enumFileCounts :: FilePath -> Enumerator Int IO b
+--
+-- It could be written with either 'joinE' or '($=)':
+--
+-- > import Data.Text as T
+-- > import Data.Enumerator.List as EL
+-- > import Data.Enumerator.Text as ET
+-- >
+-- > enumFileCounts path = joinE (enumFile path) (EL.map T.length)
+-- > enumFileCounts path = enumFile path $= EL.map T.length
+--
+-- Since: 0.4.9
+
+($=) :: Monad m
+     => Enumerator ao m (Step ai m b)
+     -> Enumeratee ao ai m b
+     -> Enumerator ai m b
+($=) = joinE
+
+
 -- | Feeds outer input elements into the provided iteratee until it yields
 -- an inner input, passes that to the inner iteratee, and then loops.
 
@@ -522,6 +586,68 @@
 isEOF = continue $ \s -> case s of
 	EOF -> yield True s
 	_ -> yield False s
+
+
+-- | Try to run an IO computation. If it throws an exception, the exception
+-- is caught and converted into an {\tt Error}.
+--
+-- Since: 0.4.9
+
+tryIO :: MonadIO m => IO b -> Iteratee a m b
+tryIO io = Iteratee $ do
+	tried <- liftIO (Exc.try io)
+	return $ case tried of
+		Right b -> Yield b (Chunks [])
+		Left err -> Error err
+
+
+-- | A common pattern in 'Enumerator' implementations is to check whether
+-- the inner 'Iteratee' has finished, and if so, to return its output.
+-- 'checkContinue0' passes its parameter a continuation if the 'Iteratee'
+-- can still consume input; if not, it returns the iteratee's step.
+--
+-- The type signature here is a bit crazy, but it's actually very easy to
+-- use. Take this code:
+--
+-- > repeat :: Monad m => a -> Enumerator a m b
+-- > repeat x = loop where
+-- > 	loop (Continue k) = k (Chunks [x]) >>== loop
+-- > 	loop step = returnI step
+--
+-- And rewrite it without the boilerplate:
+--
+-- > repeat :: Monad m => a -> Enumerator a m b
+-- > repeat x = checkContinue0 $ \loop k -> k (Chunks [x] >>== loop
+--
+-- Since: 0.4.9
+
+checkContinue0 :: Monad m
+               => (Enumerator a m b
+                -> (Stream a -> Iteratee a m b)
+                -> Iteratee a m b)
+               -> Enumerator a m b
+checkContinue0 inner = loop where
+	loop (Continue k) = inner loop k
+	loop step = returnI step
+
+
+-- | Like 'checkContinue0', but allows each loop step to use a state value:
+--
+-- > iterate :: Monad m => (a -> a) -> a -> Enumerator a m b
+-- > iterate f = checkContinue1 $ \loop a k -> k (Chunks [a]) >>== loop (f a)
+--
+-- Since: 0.4.9
+
+checkContinue1 :: Monad m
+               => ((s1 -> Enumerator a m b)
+                -> s1
+                -> (Stream a -> Iteratee a m b)
+                -> Iteratee a m b)
+               -> s1
+               -> Enumerator a m b
+checkContinue1 inner = loop where
+	loop s (Continue k) = inner loop s k
+	loop _ step = returnI step
 
 
 
diff --git a/hs/Data/Enumerator/Binary.hs b/hs/Data/Enumerator/Binary.hs
--- a/hs/Data/Enumerator/Binary.hs
+++ b/hs/Data/Enumerator/Binary.hs
@@ -40,6 +40,10 @@
 	, Data.Enumerator.Binary.concatMap
 	, concatMapM
 	
+	-- ** Accumulating maps
+	, mapAccum
+	, mapAccumM
+	
 	-- ** Infinite streams
 	, Data.Enumerator.Binary.iterate
 	, iterateM
@@ -77,11 +81,9 @@
 import Data.Enumerator hiding ( head, drop, iterateM, repeatM, replicateM
                               , generateM, filterM, consume, foldM
                               , concatMapM)
-import Data.Enumerator.Util (tryIO)
 import Control.Monad.IO.Class (MonadIO)
 import qualified Data.ByteString as B
 import qualified System.IO as IO
-import Data.Function (fix)
 import qualified Control.Exception as Exc
 import System.IO.Error (isEOFError)
 import Data.Word (Word8)
@@ -121,11 +123,9 @@
 -- Since: 0.4.8
 
 unfold :: Monad m => (s -> Maybe (Word8, s)) -> s -> Enumerator B.ByteString m b
-unfold f = loop where
-	loop s (Continue k) = case f s of
-		Nothing -> continue k
-		Just (b, s') -> k (Chunks [B.singleton b]) >>== loop s'
-	loop _ step = returnI step
+unfold f = checkContinue1 $ \loop s k -> case f s of
+	Nothing -> continue k
+	Just (b, s') -> k (Chunks [B.singleton b]) >>== loop s'
 
 
 -- | Enumerates a stream of bytes by repeatedly applying a computation to
@@ -136,13 +136,11 @@
 -- Since: 0.4.8
 
 unfoldM :: Monad m => (s -> m (Maybe (Word8, s))) -> s -> Enumerator B.ByteString m b
-unfoldM f = loop where
-	loop s (Continue k) = do
-		fs <- lift (f s)
-		case fs of
-			Nothing -> continue k
-			Just (b, s') -> k (Chunks [B.singleton b]) >>== loop s'
-	loop _ step = returnI step
+unfoldM f = checkContinue1 $ \loop s k -> do
+	fs <- lift (f s)
+	case fs of
+		Nothing -> continue k
+		Just (b, s') -> k (Chunks [B.singleton b]) >>== loop s'
 
 
 -- | @'map' f@ applies /f/ to each input byte and feeds the
@@ -189,6 +187,41 @@
 			checkDoneEx (Chunks [B.pack xs]) (\k' -> loop k' xs)
 
 
+-- | Similar to 'map', but with a stateful step function.
+--
+-- Since: 0.4.9
+
+mapAccum :: Monad m => (s -> Word8 -> (s, Word8)) -> s -> Enumeratee B.ByteString B.ByteString m b
+mapAccum f s0 = checkDone (continue . step s0) where
+	step _ k EOF = yield (Continue k) EOF
+	step s k (Chunks xs) = loop s k xs
+	
+	loop s k [] = continue (step s k)
+	loop s k (x:xs) = case B.uncons x of
+		Nothing -> loop s k xs
+		Just (b, x') -> case f s b of
+			(s', ai) -> k (Chunks [B.singleton ai]) >>==
+				checkDoneEx (Chunks (x':xs)) (\k' -> loop s' k' (x':xs))
+
+
+-- | Similar to 'mapM', but with a stateful step function.
+--
+-- Since: 0.4.9
+
+mapAccumM :: Monad m => (s -> Word8 -> m (s, Word8)) -> s -> Enumeratee B.ByteString B.ByteString m b
+mapAccumM f s0 = checkDone (continue . step s0) where
+	step _ k EOF = yield (Continue k) EOF
+	step s k (Chunks xs) = loop s k xs
+	
+	loop s k [] = continue (step s k)
+	loop s k (x:xs) = case B.uncons x of
+		Nothing -> loop s k xs
+		Just (b, x') -> do
+			(s', ai) <- lift (f s b)
+			k (Chunks [B.singleton ai]) >>==
+				checkDoneEx (Chunks (x':xs)) (\k' -> loop s' k' (x':xs))
+
+
 -- | @'iterate' f x@ enumerates an infinite stream of repeated applications
 -- of /f/ to /x/.
 --
@@ -197,9 +230,7 @@
 -- Since: 0.4.8
 
 iterate :: Monad m => (Word8 -> Word8) -> Word8 -> Enumerator B.ByteString m b
-iterate f = loop where
-	loop byte (Continue k) = k (Chunks [B.singleton byte]) >>== loop (f byte)
-	loop _ step = returnI step
+iterate f = checkContinue1 $ \loop s k -> k (Chunks [B.singleton s]) >>== loop (f s)
 
 
 -- | Similar to 'iterate', except the iteration function is monadic.
@@ -207,11 +238,10 @@
 -- Since: 0.4.8
 
 iterateM :: Monad m => (Word8 -> m Word8) -> Word8 -> Enumerator B.ByteString m b
-iterateM f base = loop (return base) where
-	loop m_byte (Continue k) = do
+iterateM f base = worker (return base) where
+	worker = checkContinue1 $ \loop m_byte k -> do
 		byte <- lift m_byte
 		k (Chunks [B.singleton byte]) >>== loop (f byte)
-	loop _ step = returnI step
 
 
 -- | Enumerates an infinite stream of a single byte.
@@ -449,16 +479,13 @@
            => Integer -- ^ Buffer size
            -> IO.Handle
            -> Enumerator B.ByteString m b
-enumHandle bufferSize h = do
+enumHandle bufferSize h = checkContinue0 $ \loop k -> do
 	let intSize = fromInteger bufferSize
 	
-	fix $ \loop step -> case step of
-		Continue k -> do
-			bytes <- tryIO (getBytes h intSize)
-			if B.null bytes
-				then continue k
-				else k (Chunks [bytes]) >>== loop
-		_ -> returnI step
+	bytes <- tryIO (getBytes h intSize)
+	if B.null bytes
+		then continue k
+		else k (Chunks [bytes]) >>== loop
 
 
 -- | Read bytes (in chunks of the given buffer size) from the handle, and
@@ -494,19 +521,20 @@
 		Just off -> tryIO (IO.hSeek h IO.AbsoluteSeek off)
 	
 	enum = case count of
-		Just n -> loop n s
+		Just n -> enumRange n s
 		Nothing -> enumHandle bufferSize h s
 	
-	loop n (Continue k) =
-		let rem = fromInteger (min bufferSize n) in
-		if rem <= 0
+	enumRange = checkContinue1 $ \loop n k -> let
+		rem = fromInteger (min bufferSize n)
+		keepGoing = do
+			bytes <- tryIO (getBytes h rem)
+			if B.null bytes
+				then continue k
+				else feed bytes
+		feed bs = k (Chunks [bs]) >>== loop (n - (toInteger (B.length bs)))
+		in if rem <= 0
 			then continue k
-			else do
-				bytes <- tryIO (getBytes h rem)
-				if B.null bytes
-					then continue k
-					else k (Chunks [bytes]) >>== loop (n - (toInteger (B.length bytes)))
-	loop _ step = returnI step
+			else keepGoing
 
 getBytes :: IO.Handle -> Int -> IO B.ByteString
 getBytes h n = do
diff --git a/hs/Data/Enumerator/List.hs b/hs/Data/Enumerator/List.hs
--- a/hs/Data/Enumerator/List.hs
+++ b/hs/Data/Enumerator/List.hs
@@ -33,6 +33,10 @@
 	, Data.Enumerator.List.concatMap
 	, concatMapM
 	
+	-- ** Accumulating maps
+	, mapAccum
+	, mapAccumM
+	
 	-- ** Infinite streams
 	, Data.Enumerator.List.iterate
 	, iterateM
@@ -114,11 +118,9 @@
 -- Since: 0.4.8
 
 unfold :: Monad m => (s -> Maybe (a, s)) -> s -> Enumerator a m b
-unfold f = loop where
-	loop s (Continue k) = case f s of
-		Nothing -> continue k
-		Just (a, s') -> k (Chunks [a]) >>== loop s'
-	loop _ step = returnI step
+unfold f = checkContinue1 $ \loop s k -> case f s of
+	Nothing -> continue k
+	Just (a, s') -> k (Chunks [a]) >>== loop s'
 
 
 -- | Enumerates a stream of elements by repeatedly applying a computation to
@@ -129,13 +131,11 @@
 -- Since: 0.4.8
 
 unfoldM :: Monad m => (s -> m (Maybe (a, s))) -> s -> Enumerator a m b
-unfoldM f = loop where
-	loop s (Continue k) = do
-		fs <- lift (f s)
-		case fs of
-			Nothing -> continue k
-			Just (a, s') -> k (Chunks [a]) >>== loop s'
-	loop _ step = returnI step
+unfoldM f = checkContinue1 $ \loop s k -> do
+	fs <- lift (f s)
+	case fs of
+		Nothing -> continue k
+		Just (a, s') -> k (Chunks [a]) >>== loop s'
 
 
 -- | @'concatMapM' f@ applies /f/ to each input element and feeds the
@@ -186,6 +186,37 @@
 mapM f = concatMapM (\x -> Prelude.mapM f [x])
 
 
+-- | Similar to 'map', but with a stateful step function.
+--
+-- Since: 0.4.9
+
+mapAccum :: Monad m => (s -> ao -> (s, ai)) -> s -> Enumeratee ao ai m b
+mapAccum f s0 = checkDone (continue . step s0) where
+	step _ k EOF = yield (Continue k) EOF
+	step s k (Chunks xs) = loop s k xs
+	
+	loop s k [] = continue (step s k)
+	loop s k (x:xs) = case f s x of
+		(s', ai) -> k (Chunks [ai]) >>==
+			checkDoneEx (Chunks xs) (\k' -> loop s' k' xs)
+
+
+-- | Similar to 'mapM', but with a stateful step function.
+--
+-- Since: 0.4.9
+
+mapAccumM :: Monad m => (s -> ao -> m (s, ai)) -> s -> Enumeratee ao ai m b
+mapAccumM f s0 = checkDone (continue . step s0) where
+	step _ k EOF = yield (Continue k) EOF
+	step s k (Chunks xs) = loop s k xs
+	
+	loop s k [] = continue (step s k)
+	loop s k (x:xs) = do
+		(s', ai) <- lift (f s x)
+		k (Chunks [ai]) >>==
+			checkDoneEx (Chunks xs) (\k' -> loop s' k' xs)
+
+
 -- | @'iterate' f x@ enumerates an infinite stream of repeated applications
 -- of /f/ to /x/.
 --
@@ -194,9 +225,7 @@
 -- Since: 0.4.8
 
 iterate :: Monad m => (a -> a) -> a -> Enumerator a m b
-iterate f = loop where
-	loop a (Continue k) = k (Chunks [a]) >>== loop (f a)
-	loop _ step = returnI step
+iterate f = checkContinue1 $ \loop s k -> k (Chunks [s]) >>== loop (f s)
 
 
 -- | Similar to 'iterate', except the iteration function is monadic.
@@ -205,11 +234,10 @@
 
 iterateM :: Monad m => (a -> m a) -> a
          -> Enumerator a m b
-iterateM f base = loop (return base) where
-	loop m_a (Continue k) = do
+iterateM f base = worker (return base) where
+	worker = checkContinue1 $ \loop m_a k -> do
 		a <- lift m_a
 		k (Chunks [a]) >>== loop (f a)
-	loop _ step = returnI step
 
 
 -- | Enumerates an infinite stream of a single element.
@@ -219,7 +247,7 @@
 -- Since: 0.4.8
 
 repeat :: Monad m => a -> Enumerator a m b
-repeat a = Data.Enumerator.List.iterate (const a) a
+repeat a = checkContinue0 $ \loop k -> k (Chunks [a]) >>== loop
 
 
 -- | Enumerates an infinite stream of element. Each element is computed by
@@ -266,13 +294,11 @@
 
 generateM :: Monad m => m (Maybe a)
           -> Enumerator a m b
-generateM getNext = loop where
-	loop (Continue k) = do
-		next <- lift getNext
-		case next of
-			Nothing -> continue k
-			Just x -> k (Chunks [x]) >>== loop
-	loop step = returnI step
+generateM getNext = checkContinue0 $ \loop k -> do
+	next <- lift getNext
+	case next of
+		Nothing -> continue k
+		Just x -> k (Chunks [x]) >>== loop
 
 
 -- | Applies a predicate to the stream. The inner iteratee only receives
diff --git a/hs/Data/Enumerator/Text.hs b/hs/Data/Enumerator/Text.hs
--- a/hs/Data/Enumerator/Text.hs
+++ b/hs/Data/Enumerator/Text.hs
@@ -38,6 +38,10 @@
 	, Data.Enumerator.Text.concatMap
 	, concatMapM
 	
+	-- ** Accumulating maps
+	, mapAccum
+	, mapAccumM
+	
 	-- ** Infinite streams
 	, Data.Enumerator.Text.iterate
 	, iterateM
@@ -88,7 +92,7 @@
 import Data.Enumerator hiding ( head, drop, generateM, filterM, consume
                               , concatMapM, iterateM, repeatM, replicateM
                               , foldM)
-import Data.Enumerator.Util (tryIO, tSpanBy, tlSpanBy, reprWord, reprChar, textToStrict)
+import Data.Enumerator.Util (tSpanBy, tlSpanBy, reprWord, reprChar, textToStrict)
 import Control.Monad.IO.Class (MonadIO)
 import qualified Control.Exception as Exc
 import Control.Arrow (first)
@@ -140,11 +144,9 @@
 -- Since: 0.4.8
 
 unfold :: Monad m => (s -> Maybe (Char, s)) -> s -> Enumerator T.Text m b
-unfold f = loop where
-	loop s (Continue k) = case f s of
-		Nothing -> continue k
-		Just (c, s') -> k (Chunks [T.singleton c]) >>== loop s'
-	loop _ step = returnI step
+unfold f = checkContinue1 $ \loop s k -> case f s of
+	Nothing -> continue k
+	Just (c, s') -> k (Chunks [T.singleton c]) >>== loop s'
 
 
 -- | Enumerates a stream of characters by repeatedly applying a computation
@@ -155,13 +157,11 @@
 -- Since: 0.4.8
 
 unfoldM :: Monad m => (s -> m (Maybe (Char, s))) -> s -> Enumerator T.Text m b
-unfoldM f = loop where
-	loop s (Continue k) = do
-		fs <- lift (f s)
-		case fs of
-			Nothing -> continue k
-			Just (c, s') -> k (Chunks [T.singleton c]) >>== loop s'
-	loop _ step = returnI step
+unfoldM f = checkContinue1 $ \loop s k -> do
+	fs <- lift (f s)
+	case fs of
+		Nothing -> continue k
+		Just (c, s') -> k (Chunks [T.singleton c]) >>== loop s'
 
 
 -- | @'map' f@ applies /f/ to each input character and feeds the
@@ -208,6 +208,41 @@
 			checkDoneEx (Chunks [T.pack xs]) (\k' -> loop k' xs)
 
 
+-- | Similar to 'map', but with a stateful step function.
+--
+-- Since: 0.4.9
+
+mapAccum :: Monad m => (s -> Char -> (s, Char)) -> s -> Enumeratee T.Text T.Text m b
+mapAccum f s0 = checkDone (continue . step s0) where
+	step _ k EOF = yield (Continue k) EOF
+	step s k (Chunks xs) = loop s k xs
+	
+	loop s k [] = continue (step s k)
+	loop s k (x:xs) = case T.uncons x of
+		Nothing -> loop s k xs
+		Just (c, x') -> case f s c of
+			(s', ai) -> k (Chunks [T.singleton ai]) >>==
+				checkDoneEx (Chunks (x':xs)) (\k' -> loop s' k' (x':xs))
+
+
+-- | Similar to 'mapM', but with a stateful step function.
+--
+-- Since: 0.4.9
+
+mapAccumM :: Monad m => (s -> Char -> m (s, Char)) -> s -> Enumeratee T.Text T.Text m b
+mapAccumM f s0 = checkDone (continue . step s0) where
+	step _ k EOF = yield (Continue k) EOF
+	step s k (Chunks xs) = loop s k xs
+	
+	loop s k [] = continue (step s k)
+	loop s k (x:xs) = case T.uncons x of
+		Nothing -> loop s k xs
+		Just (c, x') -> do
+			(s', ai) <- lift (f s c)
+			k (Chunks [T.singleton ai]) >>==
+				checkDoneEx (Chunks (x':xs)) (\k' -> loop s' k' (x':xs))
+
+
 -- | @'iterate' f x@ enumerates an infinite stream of repeated applications
 -- of /f/ to /x/.
 --
@@ -216,9 +251,7 @@
 -- Since: 0.4.8
 
 iterate :: Monad m => (Char -> Char) -> Char -> Enumerator T.Text m b
-iterate f = loop where
-	loop char (Continue k) = k (Chunks [T.singleton char]) >>== loop (f char)
-	loop _ step = returnI step
+iterate f = checkContinue1 $ \loop s k -> k (Chunks [T.singleton s]) >>== loop (f s)
 
 
 -- | Similar to 'iterate', except the iteration function is monadic.
@@ -226,11 +259,10 @@
 -- Since: 0.4.8
 
 iterateM :: Monad m => (Char -> m Char) -> Char -> Enumerator T.Text m b
-iterateM f base = loop (return base) where
-	loop m_char (Continue k) = do
+iterateM f base = worker (return base) where
+	worker = checkContinue1 $ \loop m_char k -> do
 		char <- lift m_char
 		k (Chunks [T.singleton char]) >>== loop (f char)
-	loop _ step = returnI step
 
 
 -- | Enumerates an infinite stream of a single character.
@@ -469,19 +501,18 @@
 
 enumHandle :: MonadIO m => IO.Handle
            -> Enumerator T.Text m b
-enumHandle h = loop where
-	loop (Continue k) = do
-		maybeText <- tryIO getText
-		case maybeText of
-			Nothing -> continue k
-			Just text -> k (Chunks [text]) >>== loop
-	
-	loop step = returnI step
-	getText = Exc.catch
+enumHandle h = checkContinue0 $ \loop k -> do
+	let getText = Exc.catch
 		(Just `fmap` TIO.hGetLine h)
 		(\err -> if isEOFError err
 			then return Nothing
 			else Exc.throwIO err)
+	
+	maybeText <- tryIO getText
+	case maybeText of
+		Nothing -> continue k
+		Just text -> k (Chunks [text]) >>== loop
+	
 
 
 -- | Opens a file path in text mode, and passes the handle to 'enumHandle'.
diff --git a/hs/Data/Enumerator/Util.hs b/hs/Data/Enumerator/Util.hs
--- a/hs/Data/Enumerator/Util.hs
+++ b/hs/Data/Enumerator/Util.hs
@@ -1,23 +1,12 @@
 
 {-# LANGUAGE CPP #-}
 module Data.Enumerator.Util where
-import Data.Enumerator
 
 import Data.Char (toUpper, intToDigit, ord)
 import Data.Word (Word8)
 import qualified Data.Text as T
 import qualified Data.Text.Lazy as TL
-
-import Control.Monad.IO.Class (MonadIO, liftIO)
-import qualified Control.Exception as Exc
 import Numeric (showIntAtBase)
-
-tryIO :: MonadIO m => IO b -> Iteratee a m b
-tryIO io = Iteratee $ do
-	tried <- liftIO (Exc.try io)
-	return $ case tried of
-		Right b -> Yield b (Chunks [])
-		Left err -> Error err
 
 pad0 :: Int -> String -> String
 pad0 size str = padded where
diff --git a/src/api-docs.anansi b/src/api-docs.anansi
--- a/src/api-docs.anansi
+++ b/src/api-docs.anansi
@@ -111,6 +111,54 @@
 -- Since: 0.1.1
 :
 
+:d apidoc Data.Enumerator.($=)
+-- | @enum $= enee = 'joinE' enum enee@
+--
+-- &#x201c;Wraps&#x201d; an enumerator /inner/ in an enumeratee /wrapper/.
+-- The resulting enumerator will generate /wrapper/&#x2019;s output type.
+--
+-- As an example, consider an enumerator that yields line character counts
+-- for a text file (e.g. for source code readability checking):
+--
+-- > enumFileCounts :: FilePath -> Enumerator Int IO b
+--
+-- It could be written with either 'joinE' or '($=)':
+--
+-- > import Data.Text as T
+-- > import Data.Enumerator.List as EL
+-- > import Data.Enumerator.Text as ET
+-- >
+-- > enumFileCounts path = joinE (enumFile path) (EL.map T.length)
+-- > enumFileCounts path = enumFile path $= EL.map T.length
+--
+-- Since: 0.4.9
+:
+
+:d apidoc Data.Enumerator.(=$)
+-- | @enum =$ iter = 'joinI' (enum $$ iter)
+--
+-- &#x201c;Wraps&#x201d; an iteratee /inner/ in an enumeratee /wrapper/.
+-- The resulting iteratee will consume /wrapper/&#x2019;s input type and
+-- yield /inner/&#x2019;s output type.
+--
+-- Note: if the inner iteratee yields leftover input when it finishes,
+-- that extra will be discarded.
+--
+-- As an example, consider an iteratee that converts a stream of UTF8-encoded
+-- bytes into a single 'TL.Text':
+--
+-- > consumeUTF8 :: Monad m => Iteratee ByteString m Text
+--
+-- It could be written with either 'joinI' or '(=$)':
+--
+-- > import Data.Enumerator.Text as ET
+-- >
+-- > consumeUTF8 = joinI (decode utf8 $$ ET.consume)
+-- > consumeUTF8 = decode utf8 =$ ET.consume
+--
+-- Since: 0.4.9
+:
+
 :d apidoc Data.Enumerator.(<==<)
 -- | @'(\<==\<)' = flip '(>==>)'@
 --
@@ -199,6 +247,37 @@
 -- Since: 0.1.1
 :
 
+:d apidoc Data.Enumerator.checkContinue0
+-- | A common pattern in 'Enumerator' implementations is to check whether
+-- the inner 'Iteratee' has finished, and if so, to return its output.
+-- 'checkContinue0' passes its parameter a continuation if the 'Iteratee'
+-- can still consume input; if not, it returns the iteratee's step.
+--
+-- The type signature here is a bit crazy, but it's actually very easy to
+-- use. Take this code:
+--
+-- > repeat :: Monad m => a -> Enumerator a m b
+-- > repeat x = loop where
+-- > 	loop (Continue k) = k (Chunks [x]) >>== loop
+-- > 	loop step = returnI step
+--
+-- And rewrite it without the boilerplate:
+--
+-- > repeat :: Monad m => a -> Enumerator a m b
+-- > repeat x = checkContinue0 $ \loop k -> k (Chunks [x] >>== loop
+--
+-- Since: 0.4.9
+:
+
+:d apidoc Data.Enumerator.checkContinue1
+-- | Like 'checkContinue0', but allows each loop step to use a state value:
+--
+-- > iterate :: Monad m => (a -> a) -> a -> Enumerator a m b
+-- > iterate f = checkContinue1 $ \loop a k -> k (Chunks [a]) >>== loop (f a)
+--
+-- Since: 0.4.9
+:
+
 :d apidoc Data.Enumerator.checkDone
 -- | @'checkDone' = 'checkDoneEx' ('Chunks' [])@
 --
@@ -628,6 +707,18 @@
 -- Since: 0.4.8
 :
 
+:d apidoc Data.Enumerator.Binary.mapAccum
+-- | Similar to 'map', but with a stateful step function.
+--
+-- Since: 0.4.9
+:
+
+:d apidoc Data.Enumerator.Binary.mapAccumM
+-- | Similar to 'mapM', but with a stateful step function.
+--
+-- Since: 0.4.9
+:
+
 :d apidoc Data.Enumerator.Binary.repeat
 -- | Enumerates an infinite stream of a single byte.
 --
@@ -824,6 +915,18 @@
 -- Since: 0.4.8
 :
 
+:d apidoc Data.Enumerator.List.mapAccum
+-- | Similar to 'map', but with a stateful step function.
+--
+-- Since: 0.4.9
+:
+
+:d apidoc Data.Enumerator.List.mapAccumM
+-- | Similar to 'mapM', but with a stateful step function.
+--
+-- Since: 0.4.9
+:
+
 :d apidoc Data.Enumerator.List.repeat
 -- | Enumerates an infinite stream of a single element.
 --
@@ -1063,6 +1166,18 @@
 -- Since: 0.4.8
 :
 
+:d apidoc Data.Enumerator.Text.mapAccum
+-- | Similar to 'map', but with a stateful step function.
+--
+-- Since: 0.4.9
+:
+
+:d apidoc Data.Enumerator.Text.mapAccumM
+-- | Similar to 'mapM', but with a stateful step function.
+--
+-- Since: 0.4.9
+:
+
 :d apidoc Data.Enumerator.Text.repeat
 -- | Enumerates an infinite stream of a single character.
 --
@@ -1134,4 +1249,11 @@
 -- Similar to 'iterateM'.
 --
 -- Since: 0.4.8
+:
+
+:d apidoc Data.Enumerator.tryIO
+-- | Try to run an IO computation. If it throws an exception, the exception
+-- is caught and converted into an {\tt Error}.
+--
+-- Since: 0.4.9
 :
diff --git a/src/enumerator.anansi b/src/enumerator.anansi
--- a/src/enumerator.anansi
+++ b/src/enumerator.anansi
@@ -30,10 +30,10 @@
 
 \newcommand{\io}{{\sc i/o}}
 
-\title{enumerator\_0.4.8}
+\title{enumerator\_0.4.9}
 \author{John Millikin\\
         \href{mailto:"John Millikin" <jmillikin@gmail.com>}{\tt jmillikin@gmail.com}}
-\date{March 19, 2011}
+\date{March 29, 2011}
 
 \begin{document}
 
diff --git a/src/io.anansi b/src/io.anansi
--- a/src/io.anansi
+++ b/src/io.anansi
@@ -15,16 +15,13 @@
            => Integer -- ^ Buffer size
            -> IO.Handle
            -> Enumerator B.ByteString m b
-enumHandle bufferSize h = do
+enumHandle bufferSize h = checkContinue0 $ \loop k -> do
 	let intSize = fromInteger bufferSize
 	
-	fix $ \loop step -> case step of
-		Continue k -> do
-			bytes <- tryIO (getBytes h intSize)
-			if B.null bytes
-				then continue k
-				else k (Chunks [bytes]) >>== loop
-		_ -> returnI step
+	bytes <- tryIO (getBytes h intSize)
+	if B.null bytes
+		then continue k
+		else k (Chunks [bytes]) >>== loop
 :
 
 :d binary IO
@@ -41,19 +38,20 @@
 		Just off -> tryIO (IO.hSeek h IO.AbsoluteSeek off)
 	
 	enum = case count of
-		Just n -> loop n s
+		Just n -> enumRange n s
 		Nothing -> enumHandle bufferSize h s
 	
-	loop n (Continue k) =
-		let rem = fromInteger (min bufferSize n) in
-		if rem <= 0
+	enumRange = checkContinue1 $ \loop n k -> let
+		rem = fromInteger (min bufferSize n)
+		keepGoing = do
+			bytes <- tryIO (getBytes h rem)
+			if B.null bytes
+				then continue k
+				else feed bytes
+		feed bs = k (Chunks [bs]) >>== loop (n - (toInteger (B.length bs)))
+		in if rem <= 0
 			then continue k
-			else do
-				bytes <- tryIO (getBytes h rem)
-				if B.null bytes
-					then continue k
-					else k (Chunks [bytes]) >>== loop (n - (toInteger (B.length bytes)))
-	loop _ step = returnI step
+			else keepGoing
 :
 
 :d binary IO
@@ -110,19 +108,18 @@
 |apidoc Data.Enumerator.Text.enumHandle|
 enumHandle :: MonadIO m => IO.Handle
            -> Enumerator T.Text m b
-enumHandle h = loop where
-	loop (Continue k) = do
-		maybeText <- tryIO getText
-		case maybeText of
-			Nothing -> continue k
-			Just text -> k (Chunks [text]) >>== loop
-	
-	loop step = returnI step
-	getText = Exc.catch
+enumHandle h = checkContinue0 $ \loop k -> do
+	let getText = Exc.catch
 		(Just `fmap` TIO.hGetLine h)
 		(\err -> if isEOFError err
 			then return Nothing
 			else Exc.throwIO err)
+	
+	maybeText <- tryIO getText
+	case maybeText of
+		Nothing -> continue k
+		Just text -> k (Chunks [text]) >>== loop
+	
 :
 
 :d text IO
diff --git a/src/list-analogues.anansi b/src/list-analogues.anansi
--- a/src/list-analogues.anansi
+++ b/src/list-analogues.anansi
@@ -65,67 +65,55 @@
 :d element-oriented list analogues
 |apidoc Data.Enumerator.List.unfold|
 unfold :: Monad m => (s -> Maybe (a, s)) -> s -> Enumerator a m b
-unfold f = loop where
-	loop s (Continue k) = case f s of
-		Nothing -> continue k
-		Just (a, s') -> k (Chunks [a]) >>== loop s'
-	loop _ step = returnI step
+unfold f = checkContinue1 $ \loop s k -> case f s of
+	Nothing -> continue k
+	Just (a, s') -> k (Chunks [a]) >>== loop s'
 :
 
 :d byte-oriented list analogues
 |apidoc Data.Enumerator.Binary.unfold|
 unfold :: Monad m => (s -> Maybe (Word8, s)) -> s -> Enumerator B.ByteString m b
-unfold f = loop where
-	loop s (Continue k) = case f s of
-		Nothing -> continue k
-		Just (b, s') -> k (Chunks [B.singleton b]) >>== loop s'
-	loop _ step = returnI step
+unfold f = checkContinue1 $ \loop s k -> case f s of
+	Nothing -> continue k
+	Just (b, s') -> k (Chunks [B.singleton b]) >>== loop s'
 :
 
 :d text-oriented list analogues
 |apidoc Data.Enumerator.Text.unfold|
 unfold :: Monad m => (s -> Maybe (Char, s)) -> s -> Enumerator T.Text m b
-unfold f = loop where
-	loop s (Continue k) = case f s of
-		Nothing -> continue k
-		Just (c, s') -> k (Chunks [T.singleton c]) >>== loop s'
-	loop _ step = returnI step
+unfold f = checkContinue1 $ \loop s k -> case f s of
+	Nothing -> continue k
+	Just (c, s') -> k (Chunks [T.singleton c]) >>== loop s'
 :
 
 :d element-oriented list analogues
 |apidoc Data.Enumerator.List.unfoldM|
 unfoldM :: Monad m => (s -> m (Maybe (a, s))) -> s -> Enumerator a m b
-unfoldM f = loop where
-	loop s (Continue k) = do
-		fs <- lift (f s)
-		case fs of
-			Nothing -> continue k
-			Just (a, s') -> k (Chunks [a]) >>== loop s'
-	loop _ step = returnI step
+unfoldM f = checkContinue1 $ \loop s k -> do
+	fs <- lift (f s)
+	case fs of
+		Nothing -> continue k
+		Just (a, s') -> k (Chunks [a]) >>== loop s'
 :
 
 :d byte-oriented list analogues
 |apidoc Data.Enumerator.Binary.unfoldM|
 unfoldM :: Monad m => (s -> m (Maybe (Word8, s))) -> s -> Enumerator B.ByteString m b
-unfoldM f = loop where
-	loop s (Continue k) = do
-		fs <- lift (f s)
-		case fs of
-			Nothing -> continue k
-			Just (b, s') -> k (Chunks [B.singleton b]) >>== loop s'
-	loop _ step = returnI step
+unfoldM f = checkContinue1 $ \loop s k -> do
+	fs <- lift (f s)
+	case fs of
+		Nothing -> continue k
+		Just (b, s') -> k (Chunks [B.singleton b]) >>== loop s'
 :
 
 :d text-oriented list analogues
 |apidoc Data.Enumerator.Text.unfoldM|
 unfoldM :: Monad m => (s -> m (Maybe (Char, s))) -> s -> Enumerator T.Text m b
-unfoldM f = loop where
-	loop s (Continue k) = do
-		fs <- lift (f s)
-		case fs of
-			Nothing -> continue k
-			Just (c, s') -> k (Chunks [T.singleton c]) >>== loop s'
-	loop _ step = returnI step
+unfoldM f = checkContinue1 $ \loop s k -> do
+	fs <- lift (f s)
+	case fs of
+		Nothing -> continue k
+		Just (c, s') -> k (Chunks [T.singleton c]) >>== loop s'
 :
 
 \subsection{Maps}
@@ -226,6 +214,91 @@
 			checkDoneEx (Chunks [T.pack xs]) (\k' -> loop k' xs)
 :
 
+\subsection{Accumulating maps}
+
+:d element-oriented list analogues
+|apidoc Data.Enumerator.List.mapAccum|
+mapAccum :: Monad m => (s -> ao -> (s, ai)) -> s -> Enumeratee ao ai m b
+mapAccum f s0 = checkDone (continue . step s0) where
+	step _ k EOF = yield (Continue k) EOF
+	step s k (Chunks xs) = loop s k xs
+	
+	loop s k [] = continue (step s k)
+	loop s k (x:xs) = case f s x of
+		(s', ai) -> k (Chunks [ai]) >>==
+			checkDoneEx (Chunks xs) (\k' -> loop s' k' xs)
+
+|apidoc Data.Enumerator.List.mapAccumM|
+mapAccumM :: Monad m => (s -> ao -> m (s, ai)) -> s -> Enumeratee ao ai m b
+mapAccumM f s0 = checkDone (continue . step s0) where
+	step _ k EOF = yield (Continue k) EOF
+	step s k (Chunks xs) = loop s k xs
+	
+	loop s k [] = continue (step s k)
+	loop s k (x:xs) = do
+		(s', ai) <- lift (f s x)
+		k (Chunks [ai]) >>==
+			checkDoneEx (Chunks xs) (\k' -> loop s' k' xs)
+:
+
+:d byte-oriented list analogues
+|apidoc Data.Enumerator.Binary.mapAccum|
+mapAccum :: Monad m => (s -> Word8 -> (s, Word8)) -> s -> Enumeratee B.ByteString B.ByteString m b
+mapAccum f s0 = checkDone (continue . step s0) where
+	step _ k EOF = yield (Continue k) EOF
+	step s k (Chunks xs) = loop s k xs
+	
+	loop s k [] = continue (step s k)
+	loop s k (x:xs) = case B.uncons x of
+		Nothing -> loop s k xs
+		Just (b, x') -> case f s b of
+			(s', ai) -> k (Chunks [B.singleton ai]) >>==
+				checkDoneEx (Chunks (x':xs)) (\k' -> loop s' k' (x':xs))
+
+|apidoc Data.Enumerator.Binary.mapAccumM|
+mapAccumM :: Monad m => (s -> Word8 -> m (s, Word8)) -> s -> Enumeratee B.ByteString B.ByteString m b
+mapAccumM f s0 = checkDone (continue . step s0) where
+	step _ k EOF = yield (Continue k) EOF
+	step s k (Chunks xs) = loop s k xs
+	
+	loop s k [] = continue (step s k)
+	loop s k (x:xs) = case B.uncons x of
+		Nothing -> loop s k xs
+		Just (b, x') -> do
+			(s', ai) <- lift (f s b)
+			k (Chunks [B.singleton ai]) >>==
+				checkDoneEx (Chunks (x':xs)) (\k' -> loop s' k' (x':xs))
+:
+
+:d text-oriented list analogues
+|apidoc Data.Enumerator.Text.mapAccum|
+mapAccum :: Monad m => (s -> Char -> (s, Char)) -> s -> Enumeratee T.Text T.Text m b
+mapAccum f s0 = checkDone (continue . step s0) where
+	step _ k EOF = yield (Continue k) EOF
+	step s k (Chunks xs) = loop s k xs
+	
+	loop s k [] = continue (step s k)
+	loop s k (x:xs) = case T.uncons x of
+		Nothing -> loop s k xs
+		Just (c, x') -> case f s c of
+			(s', ai) -> k (Chunks [T.singleton ai]) >>==
+				checkDoneEx (Chunks (x':xs)) (\k' -> loop s' k' (x':xs))
+
+|apidoc Data.Enumerator.Text.mapAccumM|
+mapAccumM :: Monad m => (s -> Char -> m (s, Char)) -> s -> Enumeratee T.Text T.Text m b
+mapAccumM f s0 = checkDone (continue . step s0) where
+	step _ k EOF = yield (Continue k) EOF
+	step s k (Chunks xs) = loop s k xs
+	
+	loop s k [] = continue (step s k)
+	loop s k (x:xs) = case T.uncons x of
+		Nothing -> loop s k xs
+		Just (c, x') -> do
+			(s', ai) <- lift (f s c)
+			k (Chunks [T.singleton ai]) >>==
+				checkDoneEx (Chunks (x':xs)) (\k' -> loop s' k' (x':xs))
+:
+
 \subsection{Infinite streams}
 
 {\tt iterate} and {\tt iterateM} apply a function repeatedly to the base
@@ -234,56 +307,47 @@
 :d element-oriented list analogues
 |apidoc Data.Enumerator.List.iterate|
 iterate :: Monad m => (a -> a) -> a -> Enumerator a m b
-iterate f = loop where
-	loop a (Continue k) = k (Chunks [a]) >>== loop (f a)
-	loop _ step = returnI step
+iterate f = checkContinue1 $ \loop s k -> k (Chunks [s]) >>== loop (f s)
 :
 
 :d byte-oriented list analogues
 |apidoc Data.Enumerator.Binary.iterate|
 iterate :: Monad m => (Word8 -> Word8) -> Word8 -> Enumerator B.ByteString m b
-iterate f = loop where
-	loop byte (Continue k) = k (Chunks [B.singleton byte]) >>== loop (f byte)
-	loop _ step = returnI step
+iterate f = checkContinue1 $ \loop s k -> k (Chunks [B.singleton s]) >>== loop (f s)
 :
 
 :d text-oriented list analogues
 |apidoc Data.Enumerator.Text.iterate|
 iterate :: Monad m => (Char -> Char) -> Char -> Enumerator T.Text m b
-iterate f = loop where
-	loop char (Continue k) = k (Chunks [T.singleton char]) >>== loop (f char)
-	loop _ step = returnI step
+iterate f = checkContinue1 $ \loop s k -> k (Chunks [T.singleton s]) >>== loop (f s)
 :
 
 :d element-oriented list analogues
 |apidoc Data.Enumerator.List.iterateM|
 iterateM :: Monad m => (a -> m a) -> a
          -> Enumerator a m b
-iterateM f base = loop (return base) where
-	loop m_a (Continue k) = do
+iterateM f base = worker (return base) where
+	worker = checkContinue1 $ \loop m_a k -> do
 		a <- lift m_a
 		k (Chunks [a]) >>== loop (f a)
-	loop _ step = returnI step
 :
 
 :d byte-oriented list analogues
 |apidoc Data.Enumerator.Binary.iterateM|
 iterateM :: Monad m => (Word8 -> m Word8) -> Word8 -> Enumerator B.ByteString m b
-iterateM f base = loop (return base) where
-	loop m_byte (Continue k) = do
+iterateM f base = worker (return base) where
+	worker = checkContinue1 $ \loop m_byte k -> do
 		byte <- lift m_byte
 		k (Chunks [B.singleton byte]) >>== loop (f byte)
-	loop _ step = returnI step
 :
 
 :d text-oriented list analogues
 |apidoc Data.Enumerator.Text.iterateM|
 iterateM :: Monad m => (Char -> m Char) -> Char -> Enumerator T.Text m b
-iterateM f base = loop (return base) where
-	loop m_char (Continue k) = do
+iterateM f base = worker (return base) where
+	worker = checkContinue1 $ \loop m_char k -> do
 		char <- lift m_char
 		k (Chunks [T.singleton char]) >>== loop (f char)
-	loop _ step = returnI step
 :
 
 {\tt repeat} and {\tt repeatM} create infinite streams, where each input
@@ -292,7 +356,7 @@
 :d element-oriented list analogues
 |apidoc Data.Enumerator.List.repeat|
 repeat :: Monad m => a -> Enumerator a m b
-repeat a = Data.Enumerator.List.iterate (const a) a
+repeat a = checkContinue0 $ \loop k -> k (Chunks [a]) >>== loop
 :
 
 :d element-oriented list analogues
@@ -378,13 +442,11 @@
 |apidoc Data.Enumerator.List.generateM|
 generateM :: Monad m => m (Maybe a)
           -> Enumerator a m b
-generateM getNext = loop where
-	loop (Continue k) = do
-		next <- lift getNext
-		case next of
-			Nothing -> continue k
-			Just x -> k (Chunks [x]) >>== loop
-	loop step = returnI step
+generateM getNext = checkContinue0 $ \loop k -> do
+	next <- lift getNext
+	case next of
+		Nothing -> continue k
+		Just x -> k (Chunks [x]) >>== loop
 :
 
 :d byte-oriented list analogues
diff --git a/src/public-interface.anansi b/src/public-interface.anansi
--- a/src/public-interface.anansi
+++ b/src/public-interface.anansi
@@ -70,11 +70,9 @@
 import Data.Enumerator hiding ( head, drop, iterateM, repeatM, replicateM
                               , generateM, filterM, consume, foldM
                               , concatMapM)
-import Data.Enumerator.Util (tryIO)
 import Control.Monad.IO.Class (MonadIO)
 import qualified Data.ByteString as B
 import qualified System.IO as IO
-import Data.Function (fix)
 import qualified Control.Exception as Exc
 import System.IO.Error (isEOFError)
 import Data.Word (Word8)
@@ -101,7 +99,7 @@
 import Data.Enumerator hiding ( head, drop, generateM, filterM, consume
                               , concatMapM, iterateM, repeatM, replicateM
                               , foldM)
-import Data.Enumerator.Util (tryIO, tSpanBy, tlSpanBy, reprWord, reprChar, textToStrict)
+import Data.Enumerator.Util (tSpanBy, tlSpanBy, reprWord, reprChar, textToStrict)
 import Control.Monad.IO.Class (MonadIO)
 import qualified Control.Exception as Exc
 import Control.Arrow (first)
@@ -143,6 +141,8 @@
 , ($$)
 , (>==>)
 , (<==<)
+, (=$)
+, ($=)
 
 -- ** Running iteratees
 , run
@@ -158,9 +158,12 @@
 , joinE
 , Data.Enumerator.sequence
 , enumEOF
+, checkContinue0
+, checkContinue1
 , checkDoneEx
 , checkDone
 , isEOF
+, tryIO
 
 -- ** Testing and debugging
 , printChunks
@@ -223,6 +226,10 @@
 , Data.Enumerator.Binary.concatMap
 , concatMapM
 
+-- ** Accumulating maps
+, mapAccum
+, mapAccumM
+
 -- ** Infinite streams
 , Data.Enumerator.Binary.iterate
 , iterateM
@@ -268,6 +275,10 @@
 , Data.Enumerator.List.concatMap
 , concatMapM
 
+-- ** Accumulating maps
+, mapAccum
+, mapAccumM
+
 -- ** Infinite streams
 , Data.Enumerator.List.iterate
 , iterateM
@@ -316,6 +327,10 @@
 , Data.Enumerator.Text.mapM
 , Data.Enumerator.Text.concatMap
 , concatMapM
+
+-- ** Accumulating maps
+, mapAccum
+, mapAccumM
 
 -- ** Infinite streams
 , Data.Enumerator.Text.iterate
diff --git a/src/utilities.anansi b/src/utilities.anansi
--- a/src/utilities.anansi
+++ b/src/utilities.anansi
@@ -29,6 +29,14 @@
 	check (Error e) = throwError e
 :
 
+:d unsorted utilities
+infixr 0 =$
+
+|apidoc Data.Enumerator.(=$)|
+(=$) :: Monad m => Enumeratee ao ai m b -> Iteratee ai m b -> Iteratee ao m b
+enum =$ iter = joinI (enum $$ iter)
+:
+
 {\tt joinE} is similar, except it flattens an enumerator/enumeratee pair
 into a single enumerator.
 
@@ -46,6 +54,17 @@
 		Continue _ -> error "joinE: divergent iteratee"
 :
 
+:d unsorted utilities
+infixr 0 $=
+
+|apidoc Data.Enumerator.($=)|
+($=) :: Monad m
+     => Enumerator ao m (Step ai m b)
+     -> Enumeratee ao ai m b
+     -> Enumerator ai m b
+($=) = joinE
+:
+
 {\tt sequence} repeatedly runs its parameter to transform the stream.
 
 :d unsorted utilities
@@ -104,31 +123,65 @@
 	_ -> yield False s
 :
 
+When an enumerator has to interact with the outside world, it usually
+catches any exceptions that arise, and propagate them as {\tt Error} steps
+instead. {\tt tryIO} encapsulates that pattern.
+
+:d unsorted utilities
+|apidoc Data.Enumerator.tryIO|
+tryIO :: MonadIO m => IO b -> Iteratee a m b
+tryIO io = Iteratee $ do
+	tried <- liftIO (Exc.try io)
+	return $ case tried of
+		Right b -> Yield b (Chunks [])
+		Left err -> Error err
+:
+
+Another enumerator pattern that pops up often is a loop that ignores any
+non-{\tt Continue} steps. This is especially useful when implementing
+most enumerators. It's sort of an analogue to {\tt checkDone}, so I
+called it {\tt checkContinue}. It's actually implemented by various
+functions ({\tt checkContinue0}, {\tt checkContinue1}, etc), as most
+enumerators have some sort of state to pass around.
+
+:d unsorted utilities
+|apidoc Data.Enumerator.checkContinue0|
+checkContinue0 :: Monad m
+               => (Enumerator a m b
+                -> (Stream a -> Iteratee a m b)
+                -> Iteratee a m b)
+               -> Enumerator a m b
+checkContinue0 inner = loop where
+	loop (Continue k) = inner loop k
+	loop step = returnI step
+:
+
+:d unsorted utilities
+|apidoc Data.Enumerator.checkContinue1|
+checkContinue1 :: Monad m
+               => ((s1 -> Enumerator a m b)
+                -> s1
+                -> (Stream a -> Iteratee a m b)
+                -> Iteratee a m b)
+               -> s1
+               -> Enumerator a m b
+checkContinue1 inner = loop where
+	loop s (Continue k) = inner loop s k
+	loop _ step = returnI step
+:
+
 {\tt Data.Enumerator.Util} is a hidden module for functions used by several
 public modules, but not logically part of the {\tt enumerator} API.
 
 :f Data/Enumerator/Util.hs
 {-# LANGUAGE CPP #-}
 module Data.Enumerator.Util where
-import Data.Enumerator
 
 import Data.Char (toUpper, intToDigit, ord)
 import Data.Word (Word8)
 import qualified Data.Text as T
 import qualified Data.Text.Lazy as TL
-
-import Control.Monad.IO.Class (MonadIO, liftIO)
-import qualified Control.Exception as Exc
 import Numeric (showIntAtBase)
-:
-
-:f Data/Enumerator/Util.hs
-tryIO :: MonadIO m => IO b -> Iteratee a m b
-tryIO io = Iteratee $ do
-	tried <- liftIO (Exc.try io)
-	return $ case tried of
-		Right b -> Yield b (Chunks [])
-		Left err -> Error err
 :
 
 :f Data/Enumerator/Util.hs
diff --git a/tests/Properties.hs b/tests/Properties.hs
--- a/tests/Properties.hs
+++ b/tests/Properties.hs
@@ -34,7 +34,6 @@
 tests :: [F.Test]
 tests =
 	[ test_StreamInstances
-	, test_Primitives
 	, test_Text
 	, test_ListAnalogues
 	, test_Other
@@ -132,38 +131,6 @@
 
 -- }}}
 
--- Primitives {{{
-
-test_Primitives :: F.Test
-test_Primitives = F.testGroup "Primitives"
-	[ test_Map
-	, test_ConcatMap
-	, test_MapM
-	, test_ConcatMapM
-	, test_Filter
-	, test_FilterM
-	]
-
-test_Map :: F.Test
-test_Map = test_Enumeratee "map" (EL.map id)
-
-test_ConcatMap :: F.Test
-test_ConcatMap = test_Enumeratee "concatMap" (EL.concatMap (:[]))
-
-test_MapM :: F.Test
-test_MapM = test_Enumeratee "mapM" (EL.mapM return)
-
-test_ConcatMapM :: F.Test
-test_ConcatMapM = test_Enumeratee "concatMapM" (EL.concatMapM (\x -> return [x]))
-
-test_Filter :: F.Test
-test_Filter = test_Enumeratee "filter" (EL.filter (\_ -> True))
-
-test_FilterM :: F.Test
-test_FilterM = test_Enumeratee "filterM" (EL.filterM (\_ -> return True))
-
--- }}}
-
 -- Text encoding / decoding {{{
 
 test_Text :: F.Test
@@ -435,6 +402,14 @@
 	, test_Require
 	, test_Isolate
 	, test_SplitWhen
+	, test_Map
+	, test_ConcatMap
+	, test_MapM
+	, test_ConcatMapM
+	, test_MapAccum
+	, test_MapAccumM
+	, test_Filter
+	, test_FilterM
 	]
 
 check :: Eq b => E.Iteratee a Identity b -> ([a] -> Either Exc.ErrorCall b) -> [a] -> Bool
@@ -616,6 +591,79 @@
 		split = LS.split . LS.dropFinalBlank . LS.dropDelims . LS.whenElt
 		words = BL.unpack bytes
 		in Right (map B.pack (split (== x) words), []))
+
+test_Map :: F.Test
+test_Map = test_Enumeratee "map" (EL.map id)
+
+test_ConcatMap :: F.Test
+test_ConcatMap = test_Enumeratee "concatMap" (EL.concatMap (:[]))
+
+test_MapM :: F.Test
+test_MapM = test_Enumeratee "mapM" (EL.mapM return)
+
+test_ConcatMapM :: F.Test
+test_ConcatMapM = test_Enumeratee "concatMapM" (EL.concatMapM (\x -> return [x]))
+
+test_MapAccum :: F.Test
+test_MapAccum = testListAnalogue "mapAccum"
+	(do
+		let enee = EL.mapAccum (\s ao -> (s+1, (s, ao))) 10
+		a <- E.joinI (enee $$ EL.head)
+		b <- EL.consume
+		return (a, b))
+	(\xs -> Right $ case xs of
+		[] -> (Nothing, [])
+		(x:xs') -> (Just (10, x), xs'))
+	(do
+		let enee = ET.mapAccum (\s ao -> (s+1, succ ao)) 10
+		a <- E.joinI (enee $$ EL.head)
+		b <- ET.consume
+		return (a, b))
+	(\text -> Right $ case TL.uncons text of
+		Nothing -> (Nothing, TL.empty)
+		Just (c, text') -> (Just (T.singleton (succ c)), text'))
+	(do
+		let enee = EB.mapAccum (\s ao -> (s+1, ao + s)) 10
+		a <- E.joinI (enee $$ EL.head)
+		b <- EB.consume
+		return (a, b))
+	(\bytes -> Right $ case BL.uncons bytes of
+		Nothing -> (Nothing, BL.empty)
+		Just (b, bytes') -> (Just (B.singleton (b + 10)), bytes'))
+
+
+test_MapAccumM :: F.Test
+test_MapAccumM = testListAnalogue "mapAccumM"
+	(do
+		let enee = EL.mapAccumM (\s ao -> return (s+1, (s, ao))) 10
+		a <- E.joinI (enee $$ EL.head)
+		b <- EL.consume
+		return (a, b))
+	(\xs -> Right $ case xs of
+		[] -> (Nothing, [])
+		(x:xs') -> (Just (10, x), xs'))
+	(do
+		let enee = ET.mapAccumM (\s ao -> return (s+1, succ ao)) 10
+		a <- E.joinI (enee $$ EL.head)
+		b <- ET.consume
+		return (a, b))
+	(\text -> Right $ case TL.uncons text of
+		Nothing -> (Nothing, TL.empty)
+		Just (c, text') -> (Just (T.singleton (succ c)), text'))
+	(do
+		let enee = EB.mapAccumM (\s ao -> return (s+1, ao + s)) 10
+		a <- E.joinI (enee $$ EL.head)
+		b <- EB.consume
+		return (a, b))
+	(\bytes -> Right $ case BL.uncons bytes of
+		Nothing -> (Nothing, BL.empty)
+		Just (b, bytes') -> (Just (B.singleton (b + 10)), bytes'))
+
+test_Filter :: F.Test
+test_Filter = test_Enumeratee "filter" (EL.filter (\_ -> True))
+
+test_FilterM :: F.Test
+test_FilterM = test_Enumeratee "filterM" (EL.filterM (\_ -> return True))
 
 -- }}}
 
