正确使用网线(五)

Correct use of Netwire (5)

我一直想试一试 FRP,昨天我终于硬着头皮开始尝试,首先使用 Netwire 5(这本身就是一个相当随意的选择,但我必须从某个地方开始!)。我已经达到了 "code which works" 的地步,但我注意到了一些模式,我不确定它们是否是图书馆预期使用方式的一部分,或者它们是否是我的症状我在某处做错了。

我从 this tutorial 开始,这足以让我起来 运行 非常容易 -- 我现在有一个由简单的 "incrementing number" 线控制的旋转立方体:

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 从不抑制——它应该总是 return 一些值——而 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 的两次调用。我认为(如果我的理解是正确的)这个参数是用于将事件发送到电线,并且由于我的电线不使用任何事件,所以这是 "safe",但感觉很糟糕(EDIT 也许 "events" 在这里是错误的词——教程将其描述为 "blocking values",但重点仍然存在——我从不打算阻止并且不使用e 参数在我的电线中的任何位置)。我查看是否有 stepWire 的版本,用于您知道自己从未有过事件并且即使有事件也不会响应但看不到事件的情况。我尝试使电线 e 参数 () 然后到处传递 Right (),这感觉比 undefined 稍微不那么脏,但似乎仍然不能完全代表我的意图。

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

因此,虽然我有可以工作的代码,但我仍然感到不安,我不知何故 "doing it wrong",并且由于 Netwire 5 是相当新的,因此很难找到 "idiomatic" 的示例我可以检查并查看我是否接近目标的代码。这是图书馆的用途还是我遗漏了什么?

EDIT:我已经设法通过结合 spinshouldQuit 合并为一个 Wire:

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

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

不过,我仍然需要将 Right undefined 作为输入传递给这条新线路。不可否认,现在只有其中一个,但我仍然不确定这是否是正确的方法。

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

也就是说,您忽略了重要的 Alternative 输入线的 Wire a b 实例。它提供了一个运算符 <|>,工作方式非常好:

假设我们有两条线:

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

如果w1禁止,那么

w1 <|> w2 == w2

如果w1不抑制,则

w1 <|> w2 == w1

这意味着 w1 <|> 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 mostly requires you to 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 时在前进和后退之间切换微调器。