{-# LANGUAGE NoMonomorphismRestriction #-} {-# LANGUAGE TypeOperators, FlexibleInstances, FlexibleContexts #-} {-# LANGUAGE Rank2Types #-} {-# LANGUAGE MultiParamTypeClasses #-} -- Generalizing enumerators and enumeratees and demonstrating -- many ways of composing (lists of) them module ComposeAdv where import IterateeM as I import Data.Monoid import Control.Monad import Control.Monad.Trans import Control.Monad.Identity -- for testing -- ------------------------------------------------------------------------ -- A few preliminaries and terminology -- Once we step a bit beyond Haskell98, the story can be told simpler -- A stream consumer -- It consumes the stream of elements e in the monad m -- An alias whose meaning becomes clear shortly type R e m = Iteratee e m -- A general stream producer newtype L e mi mo = L{unL :: forall a. R e mi a -> mo (R e mi a)} -- Both mi and mo parameters are meant to be monads. When they are -- equal, L e m m is isomorphic to the enumerator. -- L e m1 m2 is a monoid instance (Monad mi, Monad mo) => Monoid (L e mi mo) where mempty = L return mappend (L e1) (L e2) = L $ \i -> e1 i >>= e2 -- It is now intuitive that composing (or, mappending) enumerators -- concatenates their sources. -- We no longer need to think up the name for the operation of -- composing enumerators; we use the standard mappend. -- In L e mi mo, mo should be the same or bigger than mi: -- the producer should do all the effects that its consumer -- wants, and perhaps a few more. -- The following type class establishes the partial order among monads -- (what it means to be `bigger') class (Monad m1, Monad m2) => m1 :>=: m2 where promote :: m2 a -> m1 a -- with at least the following obvious instances. More can be added instance Monad m => m :>=: m where promote = id instance (Monad m, MonadTrans t, Monad (t m)) => t m :>=: m where promote = lift -- The particular case L e m (R e' m) is enumeratee! -- So, L e mi mo unify the notions of the enumerators and enumeratees -- (and it is even more general) -- For example, here is how we can `compose' L -- The code says, in particular, that composing an enumerator with -- an enumeratee gives an enumerator, and composing two enumeratees -- gives an enumeratee. -- The code also gives all the intermediate cases. compL :: (mo :>=: m) => L e m mo -> L e' m (R e m) -> L e' m mo compL (L e12) (L e23) = L (\ie' -> e12 (e23 ie') >>= promote . run) -- Plugging a consumer into a producer: regular functional application infixr 2 ||| {-# INLINE (|||) #-} L f ||| x = f x -- A few standard enumerators and enumeratees in the L notation takeL :: Monad m => Int -> L e m (R e m) takeL n = L (I.take n) enum1chunkL :: Monad m => [e] -> L e m m enum1chunkL str = L(enum_pure_1chunk str) -- ------------------------------------------------------------------------ -- The running example: the list of enumeratees to compose takes = [takeL 2, takeL 3, takeL 4] -- Using only 'take n' as an enumeratee to compose makes the -- example a bit contrived: normally we want to compose a variety -- of enumeratees. On the other hand, `take n' makes a neater example. -- Composition method 1 -- Enumeratees, enumerators, and other producers are monoids -- and can be mappend'ed. c1 :: Monad m => L e m (R e m) c1 = foldl1 mappend takes -- We could have used foldr1: the result is the same since -- mappend is associative. -- Mappend'ing enumerators `concatenates' their sources. -- Therefore, c1 == take (2+3+4) -- Sample runs of c1, using [1..15] as the sample input stream -- and stream2list as an inner iteratee (which reads the whole stream -- and returns it as a list) -- We compose the resulting enumeratee c1 with the enumerator enum1chunkL -- to obtain an enumerators that produces a restricted stream. -- We hook it up with the stream2list iteratee. c1r = runIdentity $ run =<< enum1chunkL [1..15] `compL` c1 ||| stream2list -- [1,2,3,4,5,6,7,8,9] -- indeed, c1 behaves as take 9 -- the same, but written differently c1r' = runIdentity $ run =<< enum1chunkL [1..15] ||| (runI =<< c1 ||| stream2list) -- [1,2,3,4,5,6,7,8,9] -- Composition method 2 -- Applying the consumer to the producer and composing the results. -- If e_j is a producer and i is an iteratee (for the inner stream), -- (e_i ||| i) is an iteratee for the outer stream. -- We may use various iteratee composers to compose (e_j i). -- For example, we use sequential composition c2s :: Monad m => R e m a -> R e m [a] c2s = \i -> sequence $ map (\e -> runI =<< e ||| i) takes c2sr = runIdentity $ run =<< enum1chunkL [1..15] ||| c2s stream2list -- [[1,2],[3,4,5],[6,7,8,9]] -- or parallel composition c2p :: Monad m => R e m a -> R e m [a] c2p = \i -> parallel $ map (\e -> runI =<< e ||| i) takes where parallel [i] = liftM (: []) i parallel (i:t) = do (iv,tv) <- enumPair i (parallel t) return (iv:tv) c2pr = runIdentity $ run =<< enum1chunkL [1..15] ||| c2p stream2list -- [[1,2],[1,2,3],[1,2,3,4]] -- Composition method 3 -- Finally, we can compose enumeratees `telescopically': -- enumeratees are stream converters, which can be arranged into a pipeline -- (very much like the Unix Shell pipeline) -- to convert the elements of the outer stream further and further. -- The term c1r showed compL composing an enumerator for the stream e -- with an enumeratee from the stream e to the stream e' giving the enumerator -- for the stream e'. That was one example of the `telescopic' composition. -- The operation compL applies to two eneumeratees just as well. -- compare with c1: compL vs mappend c4' = foldl1 compL takes c4'' = foldr1 compL takes -- Piping 'take n_j' into each other is not very interesting. It is -- easy to see that the result is equivalent to the single -- take (minimum [n_1,...n_j]) c4r' = runIdentity $ run =<< enum1chunkL [1..15] `compL` c4' ||| stream2list -- indeed, c4 behaves like take (minimum [2,3,4]) == take 2 -- [1,2] c4r'' = runIdentity $ run =<< enum1chunkL [1..15] `compL` c4'' ||| stream2list -- [1,2]