以无状态方式处理输入事件

Handle input events in stateless manner

昨天我问过如何以函数式方式处理键盘输入(使用 ScalaFX)。感谢@alfilercio 的帮助,我想到了这样的事情:

class InputHandler {
  private val goUp: Int => State => State = step => state => State(state.x, state.y - step)
  private val goDown: Int => State => State = step => state => State(state.x, state.y + step)
  private val goLeft: Int => State => State = step => state => State(state.x - step, state.y)
  private val goRight: Int => State => State = step => state => State(state.x + step, state.y)
  private val doNothing: Int => State => State = step => state => state

  private var behaviour: Int => State => State = doNothing

  def keyPressedHandler(key: KeyEvent): Unit = {
    behaviour = key.getCode match {
      case KeyCode.Up.delegate => goUp
      case KeyCode.Down.delegate => goDown
      case KeyCode.Left.delegate => goLeft
      case KeyCode.Right.delegate => goRight
      case _ => doNothing
    }
  }

  def keyReleasedHandler(key: KeyEvent): Unit = behaviour = doNothing

  def update: Int => State => State = behaviour
}

然后有一个更新程序(工作名称),它根据经过的时间更新状态,一些内部逻辑 and/or 用户输入:

def update(state: State)(implicit inputHandler: InputHandler): State = { ... }

采用这种方法,核心 类 可以保持纯净,不需要任何单一变量。但是 InputHandler 本身仍然存在问题。我的意思是行为变量使它有状态。此 InputHandler 为用于生成 GUI 的 ScalaFX 添加了某种抽象。 metdods keyPressedHandler/keyRelasedHandler 分别设置为 ScalaFX 场景事件处理程序。总而言之,我正在寻找从此 InputHandler 中删除状态变量(行为)的方法。出于教育原因,我试图掌握功能方法,这就是为什么我一直用这个案例来打扰你:)

就我个人而言,我会假设所有 ListenerHandler 根据定义都是来自我们纯净世界之外的不纯净物体,所以 如果我想让事物保持纯净,我会让他们通过一些 IO 将命令作为值发送。

class SthListener(keyPressed: KeyCode => Unit,
                  keyReleased: KeyCode => Unit,
                  updateState: (State => Unit) => Unit) externds XListener {

  def keyPressedHandler(key: KeyEvent): Unit = keyPressed(key.getCode)

  def keyReleasedHandler(key: KeyEvent): Unit = keyReleased()

  def update: Unit = updateState { state => // received from outside world
    // how to update current component basing on received state
  }
}

和其他地方

sealed trait Event
object Event {
  case class KeyPressed(keyCode: Int) extends Event
  case class KeyReleased(keyCode: Int) extends Event
}

val eventBus = Queue[Task, Event]

val stateRef = Ref[Task, State]

// translate pure operations on boundary of dirtyness
new SthListener(
  keyPressed = keyCode => eventBus.enqueue1(Event.KeyPressed(keyCode)).runSync, // or runAndForget, or what works for you,
  keyReleased = keyCode => eventBus.enqueue1(Event.KeyReleased(keyCode)).runSync,
  update => stateRef.get.runSync
)

// handle state transitions
queue.dequeue
  .evalMap {
    case Event.KeyPressed(key)  => stateRef.update(...)
    case Event.KeyReleased(key) => stateRef.update(...)
  }
  .compile
  .drain

总是这样做有意义吗?我个人不这么认为,但是 you/your 团队看到了纯度、RT 的很多价值,并且您有很多用例可以证明这种开销是合理的,那么您可以添加这种包装器以确保必要的API 不会强制您更改整个应用程序的编码风格,而只是强制您更改命令式风格的部分。

如果这太多了……好吧,您现在可以只使用可变性和命令式风格,等您感觉更熟悉后再重新审视该方法。不强求,慢慢来写当前你能看懂和维护的代码。