(PureScript) 除了 Eff,我如何在 monadic 上下文中 运行 DOM 事件侦听器回调?

(PureScript) How can I run a DOM event listener callback within a monadic context other than Eff?

我正在使用 PureScript 制作 canvas 游戏,我想知道处理事件侦听器的最佳方式是什么,尤其是 运行 自定义 monad 堆栈中的回调。这是我的游戏栈...

type BaseEffect e = Eff (canvas :: CANVAS, dom :: DOM, console :: CONSOLE | e)
type GameState = { canvas :: CanvasElement, context :: Context2D, angle :: Number }
type GameEffect e a = StateT GameState (BaseEffect e) a

我想做的是在按下任意键时更改 GameState 中的 "angle" 属性(仅用于开发目的,以便我可以调整图形)。这是我的回调函数...

changeState :: forall e. Event -> GameEffect e Unit
changeState a = do
  modify \s -> s { angle = s.angle + 1.0 }
  liftEff $ log "keypress"
  pure unit

不过 addEventListenereventListener 看起来它们只能与 Eff 一起使用,因此以下不会进行类型检查...

addEventListener
  (EventType "keypress")
  (eventListener changeState)
  false
  ((elementToEventTarget <<< htmlElementToElement) bodyHtmlElement)

我以为我可以自己定义 addEventListener 和 eventListener 并且 使用外部函数接口导入它们(将 Eff 更改为 GameEffect)。该类型已检查,但当我在浏览器中尝试 运行 时导致控制台错误。

foreign import addEventListener :: forall e. EventType -> EventListener e -> Boolean -> EventTarget -> GameEffect e Unit
foreign import eventListener :: forall e. (Event -> GameEffect e Unit) -> EventListener e

在 monad 堆栈中处理 运行 回调的最佳方法是什么?

我会为此使用 purescript-aff-coroutines。这意味着将 BaseEffect 更改为 Aff,但是任何 Eff 可以做的事 Aff 也可以做:

import Prelude

import Control.Coroutine as CR
import Control.Coroutine.Aff as CRA
import Control.Monad.Aff (Aff)
import Control.Monad.Aff.AVar (AVAR)
import Control.Monad.Eff.Class (liftEff)
import Control.Monad.Eff.Console (CONSOLE, log)
import Control.Monad.Rec.Class (forever)
import Control.Monad.State (StateT, lift, modify)
import Data.Either (Either(..))

import DOM (DOM)
import DOM.Event.EventTarget (addEventListener, eventListener)
import DOM.Event.Types (Event, EventTarget, EventType(..))
import DOM.HTML.Types (HTMLElement, htmlElementToElement)
import DOM.Node.Types (elementToEventTarget)

import Graphics.Canvas (CANVAS, CanvasElement, Context2D)

type BaseEffect e = Aff (canvas :: CANVAS, dom :: DOM, console :: CONSOLE, avar :: AVAR | e)
type GameState = { canvas :: CanvasElement, context :: Context2D, angle :: Number }
type GameEffect e = StateT GameState (BaseEffect e)

changeState :: forall e. Event -> GameEffect e Unit
changeState a = do
  modify \s -> s { angle = s.angle + 1.0 }
  liftEff $ log "keypress"
  pure unit

eventProducer :: forall e. EventType -> EventTarget -> CR.Producer Event (GameEffect e) Unit
eventProducer eventType target =
  CRA.produce' \emit ->
    addEventListener eventType (eventListener (emit <<< Left)) false target

setupListener :: forall e. HTMLElement -> GameEffect e Unit
setupListener bodyHtmlElement = CR.runProcess $ consumer `CR.pullFrom` producer
  where
  producer :: CR.Producer Event (GameEffect e) Unit
  producer =
    eventProducer
      (EventType "keypress")
      ((elementToEventTarget <<< htmlElementToElement) bodyHtmlElement)
  consumer :: CR.Consumer Event (GameEffect e) Unit
  consumer = forever $ lift <<< changeState =<< CR.await

所以在这里 eventProducer 函数为事件侦听器创建协程生成器,然后 setupListener 执行与上面的理论 addEventListener 用法等效的操作。

它的工作原理是为侦听器创建生产者,然后将其连接到消费者,消费者在收到 Event 时调用 changeState。协程处理 运行 具有 monadic 上下文,这里是你的 GameEffect monad,这就是一切正常的原因。