For what it's worth I like the proposal. Am I correct in thinking that this is something that would benefit everyone who wants to handle keyboard shortcuts in Elm?
On Monday, August 8, 2016 at 2:49:22 PM UTC+2, Janis Voigtländer wrote: > > A while back there was a thread > <https://groups.google.com/d/msg/elm-discuss/u-6aCwaJezo/fu-HMPy6CQAJ> > about filtering subscriptions. The following is related, but can also (and > probably better) be consumed and discussed independently. For those that do > have that older thread as context in mind, the following differs in two > essential ways: > > - Earlier, the discussion was about generic filtering of arbitrary > subscriptions. The following involves no genericity whatsoever. It is only > a proposal about the Keyboard API specifically. > - The earlier thread was not rooted in practice, since very little > stuff had been built yet with subscriptions. In the following, I point to > how things have played out in practice, based on uses students have made > of > the current API in projects. > > ------------------------------ > > So, on to the subject matter: > > The keyboard package > <http://package.elm-lang.org/packages/elm-lang/keyboard> currently > contains functions such as: > > Keyboard.downs : (KeyCode -> msg) -> Sub msg > > Common uses (I’ll point to several repositories below) are such that only > some keys are relevant for an application. My proposal is to have functions > such as: > > Keyboard.downsSelectively : (KeyCode -> Maybe msg) -> Sub msg > > where the semantics is that if a given KeyCode is mapped to Nothing by > the tagger, then no message gets sent along the subscription; otherwise the > Just is peeled off and the message gets sent. > ------------------------------ > > Let’s look at a practical case, https://github.com/arpad-m/dontfall. It’s > a game, where the player uses the keyboard for part of the control. > Important excerpts from the code are: > > The message type (in > https://github.com/arpad-m/dontfall/blob/master/src/BaseStuff.elm): > > type GameMsg = NothingHappened | ... several other messages ... > > The subscriptions definition (in > https://github.com/arpad-m/dontfall/blob/master/src/main.elm): > > subscriptions : GameData -> Sub GameMsgsubscriptions d = > Sub.batch > ([ Keyboard.downs (\c -> if Char.fromCode c == 'P' then PauseToogle > else NothingHappened) ] ++ > if d.state == Running then > [ AnimationFrame.diffs Tick > , Keyboard.downs (\c -> if Char.fromCode c == ' ' then > JumpDown else NothingHappened) > , Keyboard.ups (\c -> if Char.fromCode c == ' ' then JumpUp > else NothingHappened) > ] > else > []) > > The main case distinction in the main update function (in > https://github.com/arpad-m/dontfall/blob/master/src/main.elm): > > updateScene : GameMsg -> GameData -> (GameData, Cmd GameMsg)updateScene msg d > = > (case d.state of > ... > Running -> case msg of > MouseMove (x,_) -> { d | characterPosX = min x d.flWidth} > Tick t -> stepTime d t > PauseToogle -> { d | state = Paused } > JumpDown -> { d | jumpPressed = True } > JumpUp -> { d | jumpPressed = False } > _ -> d > , Cmd.none > ) > > Given availability of the functions I propose above, the code could > instead look as follows: > > type GameMsg = ... only the other messages, no NothingHappened ... > subscriptions : GameData -> Sub GameMsgsubscriptions d = > Sub.batch > ([ Keyboard.downsSelectively (\c -> if Char.fromCode c == 'P' then > Just PauseToogle else Nothing) ] ++ > if d.state == Running then > [ AnimationFrame.diffs Tick > , Keyboard.downsSelectively (\c -> if Char.fromCode c == ' ' > then Just JumpDown else Nothing) > , Keyboard.upsSelectively (\c -> if Char.fromCode c == ' ' > then Just JumpUp else Nothing) > ] > else > []) > updateScene : GameMsg -> GameData -> (GameData, Cmd GameMsg)updateScene msg d > = > (case d.state of > ... > Running -> case msg of > MouseMove (x,_) -> { d | characterPosX = min x d.flWidth} > Tick t -> stepTime d t > PauseToogle -> { d | state = Paused } > JumpDown -> { d | jumpPressed = True } > JumpUp -> { d | jumpPressed = False } > , Cmd.none > ) > > Advantages: > > 1. > > simpler message type, no special role no-op constructor needed > 2. > > no spurious update and render cycles while the game is running > 3. > > less room for bugs in the update logic > > Some additional comments on the latter two of these points: > > Re 2., given the current implementation, whenever a key is hit that is not > relevant, the update function is still called and produces an unchanged > model, which is then rendered, which is extra/useless work. Since the game > uses Graphics.*, no use can be made of Html.Lazy.* to avoid the > re-rendering. Even if something like Graphics.Lazy.* were available, > having to use it would not be as nice/pure as not causing those spurious > updates in the first place. > > Re 3., given the current implementation, there is both more room for bugs > in the now and in a potential later, when extending the game. In the now, > the programmer has to make sure that NothingHappened does indeed not > change the model. Concerning later, imagine that the programmer extends the > message type for some reason. With the current version of updateScene, > the programmer might forget to actually add a branch for handling the new > message, and the compiler would not catch that, because of the _ -> d > branch that will silently catch not only NothingHappened but also the new > message which was actually supposed to make something happen. With the > version of updateScene after the proposed change, the situation would be > different. Since there is no _ -> d branch in that Running -> case msg of > ... part anymore (thanks to NothingHappened not being a thing), the > compiler will immediately complain if the message type is extended but the > new message is not handled there. Bug prevented. > ------------------------------ > > It’s not only this single project. I have observed students applying > different strategies to deal with “Not all keys are relevant to my > program”. In each case, using an API with functions of type (KeyCode -> > Maybe msg) -> Sub msg instead of (KeyCode -> msg) -> Sub msg would have > been conceptually nicer and would have simplified things. > > Some more example repos: > > - https://github.com/chemmi/elm-rocket, uses type Key = Left | Right | > ... | NotBound and keyBinding : KeyCode -> Key and then needs to make > sure to correctly (non)-deal with NotBound in functions like > updateKeyDown; whereas just not having NotBound, but having keyBinding > : KeyCode -> Maybe Key and using that in a call to a (KeyCode -> Maybe > msg) -> Sub msg function would simplify things with the same benefits > as in the above more fully elaborated example case. > - https://github.com/Dinendal92/Abschlussprojekt-DP2016, less complete > project, but with same approach and issues as in the preceding example, > using type Key = Space | Unknown and fromCode : Int -> Key. Here, > since eliminating Unknown would turn Key into a type with only one > constructor, even more conceptual simplifications would be enabled after a > switch to the (KeyCode -> Maybe msg) -> Sub msg approach. > - https://github.com/Shaomada/Elm-Project, quite elaborate project, > structured according to TEA, uses no special Key type, instead maps > with Char.fromCode in the calls to the keyboard subscriptions, then > has to case dispatch on actual Chars at several places distributed > over the update functions of the TEA subcomponents. Subscribing with > (KeyCode > -> Maybe msg) -> Sub msg functions should allow to eliminate branches > at some of those places, removing redundancies and room for bugs. > - https://github.com/Sulring/elmaction, similar story (without TEA) > > > -- You received this message because you are subscribed to the Google Groups "Elm Discuss" group. To unsubscribe from this group and stop receiving emails from it, send an email to [email protected]. For more options, visit https://groups.google.com/d/optout.
