TL;DR
Sorry this is long: wanted to explain my approach and reasoning.
You could just skip down to My Elm Code.
Background
My father has some hand tremor. Despite it being quite mild, it nevertheless can cause him frustration when using a touch interface. Hence I decided that my apps would have button debouncing baked in (and user controllable).
Definitions
I have seen much confusion online between debouncing and throttling.
Throttling: As with the accelerator on early internal combustion cars ā otherwise known as the throttle ā throttling is the process of holding back a stream; of reducing itās flow rate. For example, there are throttling apps for Twitter that space out the tweets of prolific tweeters. But, eventually, everything that approaches a throttle goes through.
@Laurent demonstrated a neat way of throttling/delaying with
Task.perform (always msg) (Process.sleep 1000)
Debouncing: Originally, I believe, developed in the early days of electronics, when people were trying to mix electro-mechanical switches with electronic circuits. When such a switch is closed, to an unaided observer the contacts seem to close immediately. However, if viewed on high speed video slowed down, the moving contact can be seen to bounce repeatedly like a basket ball thatās been dropped to the floor. Thus sending multiple pulses when only one was required. Debouncing is the process of turning multiple inputs into just one. Anyone who has done an electronics course will have come across the Set-Reset circuit which can be used to solve this.
My approach to debouncing in Elm
Analogy: Imagine an old telegraph office where each user is rationed to one message per day. When someone asks to send a message, the clerk looks at a row of tags. If the userās tag is there, the request is declined. If the tag is not there, the request is accepted, the userās tag is hung on the row, and a 24 hour timer is started. When the timer has finished, it automatically flips the tag off of the row.
So, in Elm, the row of tags is a List of Messages.
There are three relevant branches in update:
-
Debounce message ->
Check ifmodel.debounceList
containsmessage
If it does: do nothing
If it does not: add the message to the debounce list, pass the message, wait for the debounce duration, remove the message from the list -
PassMessage message _ ->
Simply callupdate message model
-
PruneDebounceList message ->
Removemessage
frommodel.debounceList
My Elm Code (just the relevant section of update)
Debounce message ->
case List.member message debounceList of
True -> -- message is in debounce list, quietly drop message
( model, Cmd.none )
False -> -- add message to list, pass message, time, then prune
( { model | debounceList = message :: debounceList }
, Cmd.batch
[ Task.perform (PassMessage message) Time.now -- <--
, Task.perform
PruneDebounceList
( Process.sleep (toFloat debounceDuration)
|> Task.andThen (\_ -> Task.succeed message)
)
]
)
PassMessage message _ ->
update message model
PruneDebounceList message ->
let
newDebounceList = List.filter (\m -> m /= message) model.debounceList
in
( { model | debounceList = newDebounceList }, Cmd.none )
Two questions
-
Is there a better way to do this? Is this a ābadā approach?
Please interpret ābetterā any which way: more efficient / simpler / more robust / ā¦ -
Is there a way to avoid the āTime.nowā fudge?
Task.perform (PassMessage message) Time.now
-- <--
Time.now
is there only becauseTask.perform
requires a Task.