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.

Reply via email to