Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the connection between Iteratees and FRP?

It seems to me that there is a strong connection between the two ideas. My guess is that FRP could be implemented in terms of Iteratees if there would be a way to express arbitrary graphs with Iteratees. But afaik they only support chain-like structures.

Could someone shed some light on this?

like image 342
fho Avatar asked Dec 17 '12 19:12

fho


2 Answers

It's the other way around. There is a strong connection between AFRP and stream processing. In fact AFRP is a form of stream processing, and you can use the idiom to implement something very similar to pipes:

data Pipe m a b =
    Pipe {
      cleanup :: m (),
      feed    :: [a] -> m (Maybe [b], Pipe m a b)
    }

That's an extension of wire categories as found in Netwire. It receives the next chunk of input and returns Nothing when it stops producing. Using this a file reader would have the following type:

readFile :: (MonadIO m) => FilePath -> Pipe m a ByteString

Pipe is a family of applicative functors, so to apply a simple function to the stream elements you could just use fmap:

fmap (B.map toUpper) . readFile

For your convenience it's also a family of profunctors.

The most interesting feature is that this is a family of Alternative functors. That allows you to route streams around and allow multiple stream processors to "try" before giving up. This can be extended to a full-fledged parsing library that can even use some static information for optimization purposes.

like image 160
ertes Avatar answered Sep 18 '22 04:09

ertes


You can implement a limited form of FRP using stream processors. For example, using the pipes library, you might define a source of events:

mouseCoordinates :: (Proxy p) => () -> Producer p MouseCoord IO r

... and you might similarly define a graphical handler that takes mouse coordinates and updates a cursor on a canvas:

coordHandler :: (Proxy p) => () -> Consumer p MouseCoord IO r

Then you would hook up the mouse events to the handler using composition:

>>> runProxy $ mouseCoordinates >-> coordHandler

And it would run just the way you expect.

Like you said, this works well for a single chain of stages, but what about more arbitrary topologies? Well, it turns out that since the central Proxy type of pipes is a monad transformer, you can model any arbitrary topology just by nesting proxy monad transformers on top of themselves. For example, here is how you would zip two input streams:

zipD
 :: (Monad m, Proxy p1, Proxy p2, Proxy p3)
 => () -> Consumer p1 a (Consumer p2 b (Producer p3 (a, b) m)) r
zipD () = runIdentityP $ hoist (runIdentityP . hoist runIdentityP) $ forever $ do
    a <- request ()               -- Request from the outer Consumer
    b <- lift $ request ()        -- Request from the inner consumer
    lift $ lift $ respond (a, b)  -- Respond to the Producer

This behaves like a curried function. You partially apply it to each input sequentially and you can then run it when it is fully applied.

-- 1st application
p1 = runProxyK $ zipD   <-< fromListS [1..]

-- 2nd application
p2 = runProxyK $ p2     <-< fromListS [4..6]

-- 3rd application
p3 = runProxy  $ printD <-< p3

It runs just the way you expect:

>>> p3
(1, 4)
(2, 5)
(3, 6)

This trick generalizes to any topology. You can find a lot more details about this in Control.Proxy.Tutorial in the "Branches, zips, and merges" section. In particular, you should check out the fork combinator it uses as an example, which lets you split a stream into two outputs.

like image 32
Gabriella Gonzalez Avatar answered Sep 21 '22 04:09

Gabriella Gonzalez