Правильное использование Netwire (5)
Я давно хотел попробовать FRP, и вчера я наконец-то укусил пулю и начал, используя Netwire 5 для начала (довольно произвольный выбор сам по себе, но мне нужно с чего-то начинать!). Мне удалось дойти до точки "кода, который работает", но я заметил несколько шаблонов, которые, я не уверен, являются частью того, как ожидается использование библиотеки, или являются ли они признаком того, что я " Я делаю что-то не так.
Я начал с этого урока, которого было достаточно, чтобы довольно легко настроить и запустить меня - теперь у меня есть вращающийся куб, управляемый простым проводом "возрастающего числа":
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
, Я думаю (если я правильно понимаю), что этот параметр предназначен для отправки событий на провод, и что, поскольку мои проводники не используют события, это "безопасно", но это плохо (ПРАВКА, может быть, "события" неправильное слово здесь - в учебнике это описывается как "блокировка значений", но точка зрения остается неизменной - я никогда не собираюсь блокировать и не использую e
параметр где угодно в моем проводе). Я посмотрел, чтобы увидеть, была ли версия stepWire
для ситуации, когда вы знаете, что у вас никогда нет события, и вы бы не отреагировали на него, даже если бы у вас было событие, но вы его не видели. Я пытался сделать провода e
параметр ()
а затем прохождение Right ()
везде, который чувствует себя немного менее грязным, чем undefined
, но все еще не совсем отражает мое намерение.
Точно так же возвращаемое значение также является Either
, Это идеально подходит для shouldQuit
провод, но обратите внимание, я должен соответствовать шаблону на wt'
, выход из spin
провод. Я действительно не знаю, что это будет значить для этого, поэтому я просто return ()
, но я могу представить, что это становится громоздким, так как количество проводов увеличивается, и опять же, это просто не похоже на то, что представляет мое намерение - иметь провод, который никогда не запрещает и на который я всегда могу положиться, чтобы удерживать следующее значение,
Поэтому, хотя у меня есть код, который работает, у меня остается неприятное ощущение, что я как-то "делаю это неправильно", и, поскольку Netwire 5 является довольно новым, трудно найти примеры "идиоматического" кода, который я могу проверить и посмотреть, если я рядом с отметкой. Это как библиотека предназначена для использования или я что-то упустил?
РЕДАКТИРОВАТЬ: мне удалось решить вторую проблему, которую я упоминаю (сопоставление с образцом на Either
Результат spin
) путем объединения spin
а также shouldQuit
в один 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
Переход по этому проводу дает мне гораздо более разумное возвращаемое значение - если это Left
Я могу выйти, в противном случае у меня есть часть данных для работы. Это также намекает на большую степень сочетаемости, чем мой оригинальный метод.
Я все еще должен пройти Right undefined
как вход для этого нового провода, хотя. Правда, сейчас есть только один из них, но я все еще не уверен, что это правильный подход.
1 ответ
На самом верхнем уровне вашей программы у вас будет провод, который имеет (сокращенно) тип Wire a b
, Это должно быть передано что-то типа a
и он вернет что-то типа b
каждый раз, когда вы делаете шаг. Например, оба a
а также b
может быть какой-то 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
будет тормозить только если оба w1
а также w2
тормозят. Это здорово, это означает, что мы можем делать такие вещи, как:
spin :: (HasTime t s, Monad m) => Wire s e m a GL.GLfloat
spin = integral 0 . (10 . keyPressed GLFW.Key'F <|> 5)
Когда вы нажимаете F
, вы будете вращаться в два раза быстрее!
Если вы хотите изменить семантику провода после нажатия кнопки, вам нужно быть немного более креативным, но не намного. Если ваш провод ведет себя по-разному, это означает, что вы делаете какое-то переключение. Документация для выключателей в основном требует от вас следовать типам.
Вот провод, который будет действовать как провод идентификации, пока вы не нажмете данную клавишу, а затем заблокирует навсегда:
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
,