Yesterday I`ve asked how may I handle keyboard input (with ScalaFX) in a functional manner. Thanks to @alfilercio help I came up with something like this:
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
}
Then there is an Updater (working name) thats updates state based on time passed, some internal logic and/or user input:
def update(state: State)(implicit inputHandler: InputHandler): State = { ... }
With such approach the core classes may remain pure and no single variable is needed. But there is still problem with the InputHandler itself. I mean the behaviour variable makes it statefull. This InputHandler adds kind of abstraction to ScalaFX used to generate GUI. The metdods keyPressedHandler/keyRelasedHandler are set as ScalaFX Scene events handlers respectively. To conclude, I am looking way to remove state variable (behaviour) from this InputHandler. I try to grasp functional approach for educational reasons that`s why I keep bothering you with this case :)
Personally I would assume that all Listener
s and Handler
s are by definition impure objects from outside of our pure world, so if I wanted to keep things pure, I would make them send commands as values through some 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
}
}
and somewhere else
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
Does it make sense doing it always? Personally I don't think so, but you/your team see much value in purity, RT, and you have a lot of use cases which would justify this overhead, then you could add this kind of wrappers to make sure that imperative API won't force you to change the style of coding in whole application, but just in the part that forces imperative style on you.
If this is way too much... well, you can just use mutability and imperative style for now and revisit that approach once you will feel more familiar. Don't force it, take your time and write the code that the current you can understand and maintain easily.