【问题标题】:Haskell Netwire: wires of wiresHaskell Netwire:电线的电线
【发布时间】:2015-05-09 05:54:34
【问题描述】:

我正在使用 netwire 包尝试了解 FRP,我有一个简单的问题。

从以下简单的线路开始,我可以每 5 秒(大约)发出一个事件

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'

这很好而且很直接,但是我接下来要做的是将每个产生的事件映射到一个线路上,然后我可以观看更新。我有一个累加器函数,如下所示:

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

然后我引入一个新的线,它将在eventList 开始触发事件之前进行抑制,如下所示:

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

所以我已经从事件转到包含电线列表的电线。最后,我引入了一条线来对这些线中的每一个进行步进并生成一个结果列表:

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

最后,我有我的主要例程来测试它:

main = testWire clockSession_ myNumList

我希望看到的是一个不断增长的列表,其中列表中的每个元素将显示其创建时间为 10 秒,之后元素将显示为零。相反,我得到的是不断增长的静态值列表。例如,我期望在几个步骤后看到的是

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

等等。我实际看到的是

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

所以我知道我的累加器功能正在工作:创建的每个事件都被转换成一条线。但我没有看到这些电线随着时间的推移发出不同的值。我的声明 for 10 . pure x --&gt; pure 0 应该在时间过去后将它们切换为发射 0。

我还是 FRP 的新手,所以我可能从根本上误解了一些重要的东西(可能是这种情况。)

【问题讨论】:

  • 我认为 netwire 或任何其他现代 FRP 库都不可能做到这一点。这些库通过避免提供Monad 实例或任何等效项来共同努力避免时间和空间泄漏。这意味着通常不可能将Wire ... (Wire ... a) 变成Wire a

标签: haskell frp netwire


【解决方案1】:

问题是从事件生成的连线不是持久的。 Wire s e m a b 类型的给定值实际上是一个函数的时间实例,该函数从a 类型的值生成b 类型的值。由于 Haskell 使用不可变的值,为了步进线,你必须对来自 stepWire 的结果线做一些事情,否则你会为相同的输入得到相同的输出。看看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

当您对这些连线中的每一个进行步进时,您每次只会得到[.., 10, 5, 0],因为您正在重用for 10 . pure x --&gt; pure 0 连线的原始值。看stepWire的签名:

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

这意味着对于这样的语句

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

...下次需要调用stepWire时应该使用w',因为它是下一个实例的行为。如果您有生产电线的电线,那么您需要将生产的电线卸载到某个地方,以便可以单独处理它们。

有关(我相信)为您提供所需行为的程序,请参阅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】:

    作为Mokosha suggested,我们可以保留我们从以前的事件中已经知道的电线的状态,并从最新的事件中引入电线。例如,如果我们已经知道没有事件,并且得到一个包含一根线的列表,我们应该使用新的线。

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

    如果我们已经知道一根或多根电线并发现更多电线,我们需要保留我们已经知道的电线并添加我们刚刚发现的新电线。

                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
    

    这更容易从列表的前端定义。第一个参数将是结果的前缀。如果还有第二个参数,我们会将它们添加到列表的末尾。

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

    我们可以通过反转输入和输出来为列表的后端定义相同的内容。第一个参数将是结果的后缀,如果第二个参数有多余的,则将它们添加到列表的前面。

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

    现在我们可以实现myNums 来跟踪我们已经拥有哪个oldWires 的状态。

    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))
    

    如果我们想学究气,我们应该使用Maybe 电线列表,这样当电线不再存在时,我们可以在其位置留下Nothing,以便电线列表仍然匹配。如果我们这样做,即使对状态进行了巧妙的表示,当电线死亡时,我们也会泄漏空间。他们的原始定义仍将保留在eventList 积​​累的列表中。

    这给出了所需的输出

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-09-02
      • 2015-01-19
      • 1970-01-01
      • 2013-12-18
      • 2014-05-25
      • 2014-07-06
      • 2021-08-12
      • 2023-04-06
      相关资源
      最近更新 更多