Haskell Netwire: wires of wires

2019-05-09 03:25发布

问题:

I'm playing around with the netwire package trying to get a feel for FRP, and I have a quick question.

Starting with the following simple wires, I'm able to emit an event every 5 seconds (approx)

myWire :: (Monad m, HasTime t s) => Wire s () m a Float
myWire = timeF

myWire' :: (Monad m, HasTime t s) => Wire s () m a Int
myWire' = fmap round myWire

myEvent :: (Monad m, HasTime t s) => Wire s () m a (Event Int)
myEvent = periodic 5 . myWire'

This is pretty nice and straight forward, but what I want to do next is map each event produced to a wire, that I can then watch update. I have an accumulator function like the following:

eventList :: (Monad m, HasTime t s) 
            => Wire s () m a (Event [Wire s () m a Int])
eventList = accumE go [] . myEvent
  where go soFar x = f x : soFar
        f x = for 10 . pure x --> pure 0

I then introduce a new wire that will inhibit until the eventList starts triggering events, like so:

myList :: (Monad m, HasTime t s) => Wire s () m a [Wire s () m a Int]
myList = asSoonAs . eventList

So I've gone from events to a wire containing a list of wires. Finally, I introduce a wire to step each of these wires and produce a list of results:

myNums :: (Monad m, HasTime t s) => Wire s () m [Wire s () m a Int] [Int]
myNums = mkGen $ \dt wires -> do
  stepped <- mapM (\w -> stepWire w dt $ Right undefined) wires
  let alive = [ (r, w) | (Right r, w) <- stepped ]
  return (Right (map fst alive), myNums)

myNumList :: (Monad m, HasTime t s) => Wire s () m a [Int]
myNumList = myNums . myList

And finally, I have my main routine to test it all out:

main = testWire clockSession_ myNumList

What I expect to see is a growing list, where each element in the list will show it's creation time for 10 seconds, after which the element will show a zero. What I'm getting instead is a growing list of static values. For example, what I expect to see after a few steps is

[0]
[5, 0]
[10, 5, 0]
[15, 10, 0, 0]

and so on. What I'm actually seeing is

[0]
[5, 0]
[10, 5, 0]
[15, 10, 5, 0]

So I know my accumulator function is working: every event created is being converted into a wire. But what I'm not seeing are these wires emitting different values over time. My statement for 10 . pure x --> pure 0 should be switching them over to emitting 0 after the time has elapsed.

I am still new to FRP, so I may be fundamentally misunderstanding something important about it (probably the case.)

回答1:

The problem is that the wires generated from the events are not persistent. A given value of type Wire s e m a b is actually an instance in time of a function that produces a value of type b from a value of type a. Since Haskell uses immutable values, in order to step wires, you have to do something with the resulting wire from stepWire otherwise you get the same output for the same input. Take a look at the results from myList:

Event 1: [for 10 . pure 0 --> pure 0]
Event 2: [for 10 . pure 5 --> pure 0, for 10 . pure 0 --> pure 0]
Event 3: [for 10 . pure 10 --> pure 0, for 10 . pure 5 --> pure 0, for 10 . pure 0 --> pure 0]
... etc

When you step each of these wires, you're only going to get [.., 10, 5, 0] every time because you're reusing the original value of the for 10 . pure x --> pure 0 wire. Look at the signature for stepWire:

stepWire :: Monad m => Wire s e m a b -> s -> Either e a -> m (Either e b, Wire s e m a b)

This means that for a statement such as

(result, w') <- stepWire w dt (Right underfined)

... the w' should be used the next time you need to call stepWire, because it is the behavior at the next instance in time. If you have a wire that produces wires, then you need to offload the produced wires somewhere so that they can be processed separately.

For a program that (I believe) gives you the behavior you want, please refer to this code.

$ ghc -o test test.hs
[1 of 1] Compiling Main             ( test.hs, test.o )
Linking test ...
$ ./test
[0]
[5,0]
[10,5,0]
[15,10,0,0]
[20,15,0,0,0]
...


回答2:

As Mokosha suggested, we can keep the state of the wires we already know about from previous events and introduce the wires from the newest event. For example, if we already know about no events, and get a list with one wire in it, we should use the new wire.

      [] -- we already know about
w0' : [] -- we got as input
w0' : [] -- what we should keep

If we already know about a wire or wires and find out about even more wires, we need to keep the wires we already know about and add the new wires we just found out about.

            w2  : w1  : w0  : [] -- we already know about
w4' : w3' : w2' : w1' : w0' : [] -- we got as input
w4' : w3' : w2  : w1  : w0  : [] -- what we should keep

This is easier to define from the front end of the list. The first argument will be a prefix of the result. If there are any of the second argument left over, we'll add them to the end of the list.

makePrefixOf :: [a] -> [a] -> [a]
makePrefixOf [] ys = ys
makePrefixOf xs [] = xs
makePrefixOf (x:xs) (_:ys) = x:makePrefixOf xs ys

We can define the same thing for the back end of a list by reversing the inputs and the outputs. The first argument will be a suffix of the result, if there are any extra of the second agument, they are added to the front of the list.

makeSuffixOf :: [a] -> [a] -> [a]
makeSuffixOf xs ys = reverse $ makePrefixOf (reverse xs) (reverse ys)

Now we can implement myNums keeping track of which oldWires we already have state for.

myNums :: (Monad m, HasTime t s) => Wire s () m [Wire s () m a b] [b]
myNums = go []
  where
    go oldWires = mkGen $ \dt newWires -> do
        let wires = makeSuffixOf oldWires newWires
        stepped <- mapM (\w -> stepWire w dt $ Right undefined) wires
        let alive = [ (r, w) | (Right r, w) <- stepped ]
        return (Right (map fst alive), go (map snd alive))

If we want to be pedantic, we should use a list of Maybe wires so that when the wire is no longer alive we can leave behind a Nothing in its place so that the lists of wires still match up. If we do this, even with a clever representation for the state, we will leak space when wires die. Their original definition will still be hanging around in the list being accumulated by eventList.

This gives the desired output of

            [ 0]
         [ 5, 0]
      [10, 5, 0]
   [15,10, 0, 0]
[20,15, 0, 0, 0]