Question

I'm trying to understand how pipes-parse 3.0 works for cases besides span and splitAt, and can't quite figure out how to get things working. The basic idea is that I have an isomorphism, and I'd like to map all input values to convert from type A to type B. Then, I'd like all leftovers to be converted back from B to A. How would I accomplish this in pipes-parse?

For comparison, the code would look like the following in conduit:

import           Control.Applicative ((<$>), (<*>))
import           Data.Conduit        (yield, ($$), (=$=))
import           Data.Conduit.Extra  (fuseLeftovers)
import qualified Data.Conduit.List   as CL

newtype A = A Int
    deriving Show
newtype B = B Int
    deriving Show

atob (A i) = (B i)
btoa (B i) = (A i)

main :: IO ()
main = do
    let src = mapM_ (yield . A) [1..10]
    res <- src $$ (,,,)
        <$> fuseLeftovers (map btoa) (CL.map atob) CL.peek
        <*> CL.take 3
        <*> (CL.map atob =$= CL.take 3)
        <*> CL.consume
    print res

EDIT: To clarify, here's the output of my above code:

(Just (B 1),[A 1,A 2,A 3],[B 4,B 5,B 6],[A 7,A 8,A 9,A 10])

Note that the original stream is of type A. We're converting to B and peeking at the first element, then taking the next 3 elements as type A, then taking the following three as B, and finally taking the remainder as A.

Was it helpful?

Solution

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))
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top