{-# 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]