2017-02-07 7 views
3

Я делаю холст-игру с использованием PureScript, и мне интересно, как лучше всего обработать прослушиватели событий, в частности, выполнение обратных вызовов в пользовательском стеке монады. Это моя игра стек ...(PureScript) Как запустить обратный вызов прослушивателя событий DOM в монадическом контексте, отличном от Eff?

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 при нажатии любой клавиши (только для целей развития, так что я могу настроить графику). Это моя функция обратного вызова ...

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

Однако addEventListener и eventListener выглядят как они предназначены для использования только с 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 

Каков наилучший способ обработки обратных вызовов в стеке монады?

ответ

3

Я бы использовал 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 использование у вас было выше.

Это работает, создавая производителя для слушателя, а затем подключая его к потребителю, который вызывает changeState, когда он получает Event. Процессы Coroutine работают с монадическим контекстом, вот ваша монашка GameEffect, поэтому все получается.

+0

Мне удалось заставить его работать, но только для одного нажатия клавиши и нажатия клавиши, казалось, блокировали рендеринг графики. После первого нажатия клавиши потребитель закрывается и выводит графику. Я думаю, мне нужно создать экземпляр 'Parallel' для GameEffect (завернутый в newtype), поэтому я могу использовать' CR.runProcess $ connect производитель-потребитель'. Это звучит так, будто я на правильном пути? – Albtzrly

+0

Упс! «Пользователь» должен был быть определен следующим образом: 'навсегда $ lift <<< changeState = << CR.await', с' forever', исходящим из 'Control.Monad.Rec.Class'. –

+0

Блокировка немного сложнее решить, так как простой ответ будет [«просто fork it»] (https://github.com/slamdata/purescript-fork), однако нет экземпляра forking 'StateT'. Возможно, вы могли бы придумать что-то с 'newtype' для GameEffect. Я немного удивлен тем, что он блокирует рендеринг, поскольку это предполагает вызов 'setupListener' в цикле рендеринга? Похоже, вы должны просто сделать это один раз в конце настройки. В принципе, у меня нет хорошего ответа на этот бит, извините! –

Смежные вопросы