Обрабатывать входные события без сохранения состояния

Вчера я спросил, как я могу функционально обрабатывать ввод с клавиатуры (с помощью 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
}

Затем есть средство обновления (рабочее название), которое обновляет состояние в зависимости от прошедшего времени, некоторой внутренней логики и/или пользовательского ввода:

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

При таком подходе основные классы могут оставаться чистыми, и не требуется ни одной переменной. Но все еще есть проблема с самим InputHandler. Я имею в виду, что переменная поведения делает его с полным состоянием. Этот InputHandler добавляет некоторую абстракцию в ScalaFX, используемую для создания графического интерфейса. Метдоды keyPressedHandler/keyRelasedHandler устанавливаются как обработчики событий сцены ScalaFX соответственно. В заключение я ищу способ удалить переменную состояния (поведение) из этого InputHandler. Я пытаюсь понять функциональный подход из образовательных соображений, поэтому постоянно беспокою вас этим случаем :)


person ayeo    schedule 18.05.2020    source источник


Ответы (1)


Лично я бы предположил, что все Listener и Handler по определению являются нечистыми объектами из-за пределов нашего чистого мира, поэтому, если бы я хотел сохранить чистоту, я бы заставил их отправлять команды как значения через некоторый ввод-вывод.

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

Есть ли смысл делать это всегда? Лично я так не думаю, но вы/ваша команда видите большую ценность в чистоте, RT, и у вас есть много вариантов использования, которые оправдали бы эти накладные расходы, тогда вы могли бы добавить такие оболочки, чтобы убедиться, что императивный API выиграл не заставит вас изменить стиль кодирования во всем приложении, а только в той части, которая навязывает вам императивный стиль.

Если это слишком много ... что ж, вы можете просто использовать изменчивость и императивный стиль на данный момент и вернуться к этому подходу, как только вы почувствуете себя более знакомым. Не форсируйте это, не торопитесь и напишите код, который вы можете легко понять и поддерживать.

person Mateusz Kubuszok    schedule 18.05.2020
comment
большое спасибо за ваш развернутый ответ. Думаю, я понял общую идею. Но я не уверен в роли stateRef. Не могли бы вы объяснить эту часть более подробно? редактировать: я понял :) - person ayeo; 18.05.2020
comment
Да, забыл написать, что в этом примере я использовал FS2, Cats Effect и Monix по привычке, извините за это. - person Mateusz Kubuszok; 18.05.2020
comment
Еще лучше, новые вещи, чтобы проверить :) Еще раз спасибо из Гливице :) - person ayeo; 18.05.2020
comment
Рад помочь от Żory :) - person Mateusz Kubuszok; 18.05.2020
comment
Я знаю, что подписался на вас на Github — поэтому я упомянул Гливице ;) - person ayeo; 19.05.2020