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?
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With