【问题标题】:Correct use of Netwire (5)正确使用网线(五)
【发布时间】:2023-06-17 16:47:01
【问题描述】:

我一直想试一试 FRP,昨天我终于硬着头皮试了一下,使用 Netwire 5 开始(这本身就是一个相当随意的选择,但我必须从某个地方开始!)。我已经设法达到“有效的代码”的目的,但我注意到一些模式,我不确定这些模式是否是预期如何使用库的一部分,或者它们是否是我的症状我在某处做错了什么。

我从this tutorial 开始,这足以让我轻松启动和运行——我现在有一个由简单的“递增数字”线控制的旋转立方体:

spin :: (HasTime t s, Monad m) => Wire s e m a GL.GLfloat
spin = integral 0 . 5

当按下“Esc”时应用程序将退出,使用netwire-input-glfw中提供的电线:

shouldQuit :: (Monoid e, Functor m, Monad m) => Wire s e (GLFWInputT m) a a
shouldQuit = keyPressed GLFW.Key'Escape

它们之间的一个重要区别是spin 从不禁止——它应该总是返回一些值——而shouldQuit 一直在禁止;直到实际按下该键,在这种情况下我退出应用程序。

让我感到不安的是我最终不得不使用这些电线的方式。现在,它看起来像这样:

(wt', spinWire') <- stepWire spinWire st $ Right undefined
((qt', quitWire'), inp'') <- runStateT (stepWire quitWire st $ Right undefined) inp'

case (qt', wt') of
  (Right _, _) -> return ()
  (_, Left _)  -> return () -- ???
  (_, Right x) -> --do things, render, recurse into next frame

这种模式有两点让我感到不舒服。首先,我将Right undefined 传递给stepWire 的两个调用。我认为(如果我的理解是正确的)该参数用于将事件发送到线路,并且由于我的线路不使用任何事件,这是“安全的”,但感觉很糟糕(EDIT 也许“事件”在这里是错误的词——本教程将其描述为“阻塞值”,但重点仍然存在——我从不打算阻塞,也不打算在任何地方使用 e 参数我的电线)。我查看了是否有stepWire 的版本,用于您知道自己从未有过事件并且即使您确实有事件但看不到事件也不会响应它的情况。我尝试制作电线e 参数(),然后到处传递Right (),感觉比undefined 稍微不那么脏,但似乎仍然不能代表我的意图。

同样,返回值也是Either。这非常适合shouldQuit 线,但请注意我必须在wt' 上进行模式匹配,spin 线的输出。我真的不知道这意味着什么,所以我只是return (),但我可以想象随着电线数量的增加,这会变得笨拙,而且,它似乎并不代表我的意图-- 拥有一条永不抑制的电线,我可以一直依靠它来保持下一个值。

因此,尽管我的代码可以运行,但我还是有一种不安的感觉,即我以某种方式“做错了”,而且由于 Netwire 5 相当新,因此很难找到我能找到的“惯用”代码示例检查一下,看看我是否接近标记。这是该库的用途还是我遗漏了什么?

编辑:通过将spinshouldQuit 组合成一个@,我设法解决了我提到的第二个问题(spinEither 结果的模式匹配) 987654345@:

shouldContinuePlaying :: (Monoid e, Functor m, Monad m) => Wire s e (GLFWInputT m) a a
shouldContinuePlaying = keyNotPressed GLFW.Key'Escape

game :: (HasTime t s, Monoid e, Functor m, Monad m) => Wire s e (GLFWInputT m) a GL.GLfloat
game = spin . shouldContinuePlaying

步进这条线给了我一个更明智的返回值——如果它是Left,我可以退出,否则我有数据可以使用。它还暗示了比我原来的方法更高程度的可组合性。

尽管如此,我仍然必须将 Right undefined 作为输入传递给这条新线路。诚然,现在只有一个,但我仍然不确定这是否是正确的方法。

【问题讨论】:

  • 我倾向于不在电线中使用效果。事件被传递给“主线”,而 monad 是多态的。

标签: haskell frp netwire


【解决方案1】:

在程序的最顶层,您将有一些(缩写)类型为Wire a b 的线。这将需要传递a 类型的东西,并且每次你迈出一步时它都会返回b 类型的东西。例如,ab 都可以是游戏的WorldState,或者物理模拟器的[RigidBody]。在我看来,在顶层通过Right undefined 是可以的。

话虽如此,您忽略了输入线的重要Alternative 实例Wire a b。它提供了一个以非常好的方式工作的运算符&lt;|&gt;

假设我们有两条线:

w1 :: Wire a b
w2 :: Wire a b

如果 w1 禁止,则

w1 <|> w2 == w2

如果 w1 不禁止,则

w1 <|> w2 == w1

这意味着w1 &lt;|&gt; w2 只有在 w1w2 都被禁止时才会禁止。太好了,这意味着我们可以执行以下操作:

spin :: (HasTime t s, Monad m) => Wire s e m a GL.GLfloat
spin = integral 0 . (10 . keyPressed GLFW.Key'F <|> 5)

当您按下F 时,您的旋转速度会提高一倍!

如果你想在按下按钮后改变电线的语义,你必须更有创意,但不多。如果你的电线表现不同,这意味着你正在做某种开关。 documentation for switches 主要要求你follow the types

在您按下给定键之前,这条线的作用类似于身份线,然后将永远禁止:

trigger :: GLFW.Key -> GameWire a a
trigger key =
  rSwitch mkId . (mkId &&& ((now . pure mkEmpty . keyPressed key) <|> never))

有了这个,你可以做一些很酷的事情,比如:

spin :: (HasTime t s, Monad m) => Wire s e m a GL.GLfloat
spin = integral 0 . spinSpeed
  where
    spinSpeed = 5 . trigger GLFW.Key'F --> 
                -5 . trigger GLFW.Key'F -->
                spinSpeed

这将在您点击F 时在前进和后退之间切换微调器。

【讨论】: