I did it by introducing an auxiliary lens combinator, piso :: Iso' a b -> Iso' (Producer a m r) (Producer b m r)
import Control.Applicative
import Control.Lens (view, from, zoom, iso, Iso')
import Control.Monad.State.Strict (evalState)
import Pipes
import Pipes.Core as Pc
import qualified Pipes.Parse as Pp
import qualified Pipes.Prelude as P
newtype A = A Int
deriving Show
newtype B = B Int
deriving Show
atob (A i) = B i
btoa (B i) = A i
ab :: Iso' A B
ab = iso atob btoa
piso :: Monad m => Iso' a b -> Iso' (Producer a m r) (Producer b m r)
piso i = iso (P.map (view i) <-<) (>-> P.map (view $ from i))
main :: IO ()
main = do
let src = P.map atob <-< P.map A <-< each [1..10]
let parser = (,,) <$> zoom (Pp.splitAt 1) Pp.peek
<*> zoom (Pp.splitAt 3 . piso (from ab)) Pp.drawAll
<*> Pp.drawAll
let res = evalState parser src
print res
Here src
is a Producer B m r
and parser
a Parser B m (Maybe B, [A], [B])
. I think the heart of this is that leftovers are just what happens in the Parser
-State bound Producer
after some prior parsing actions. You can thus use zoom
just like normal to modify that Producer
however you like.
Note that we could flip the order of the lenses and do zoom (piso (from ab) . Pp.splitAt 3) Pp.drawAll
but since lenses descend from left to right that means that we're modifying the entire Producer
prior to focusing on the next three elements. Using the order in my primary example reduces the number of mappings between A
and B
.
view (Pp.splitAt 3 . piso (from ab))
:: Monad m => Producer B m x -> (Producer A m (Producer B m x))
-- note that only the outer, first Producer has been mapped over, the protected,
-- inner producer in the return type is isolated from `piso`'s effect
view (piso (from ab) . Pp.splitAt 3)
:: Monad m => Producer B m x -> (Producer A m (Producer A m x))