Gizra/elm-debouncer

The most comprehensive debouncer for Elm


License
MIT
Install
elm-package install Gizra/elm-debouncer 1.0.0

Documentation

Build Status

elm-debouncer

"Debouncing" is the process of taking a stream of inputs, over time, and emitting some kind of output, at certain intervals. So, it lets you "smooth out" events, so you have some control over how soon (or how fast) they are processed.

One classic use-case is auto-complete while typing. As each character is typed, you could send a request to a server asking for possible completions. But you don't want to slow things down while a person is typing quickly. So, you can debounce the server request, so that it will only actually take place once nothing has been typed for a few moments. By doing so, you've taken a stream of inputs over time (the characters being typed), and you've emitted an output at an appropriate moment, defined by some kind of interval.

This is, of course, a familiar concept, and there are many debounce modules available for Elm already. My favourites are:

So, why another one?

One practical concern was that I needed an easy way to "cancel" a pending event. (You could implement cancelation on top of other debouncers, but it appeared that it would not be entirely straightforward). It also seemed that it would be possible to consolidate different approaches to debouncing in an interesting way.

If that's enough introduction for you, do feel free to skip to the detailed API, at the Elm package site, or the links to the right, if you're already there. If, on the other hand, you'd like some more philosophizing about debouncers, read on!

Grouping by time

Essentially, debouncing is the processing of organizing things in groups over time. You might analogize it to the groupsOf function from elm-community/list-extra

groupsOf : Int -> List a -> List (List a)

Consider what groupsOf does. It takes a list, and hands you back the same things, just organized differently. Debouncing is conceptually similar. It takes some inputs, and (eventually) hands them back to you, just organized differently. Now, with debouncing, this doesn't happen all at once -- you don't have the whole list at once. Instead, it happens over time. So, you might say that debouncing is the process of grouping events according to time.

The idea of grouping events by time implies several questions:

  • What's the time interval?

    If we're grouping things over time, then the first question you might ask is: over what period of time? This would be like the first parameter to groupsOf -- that is, in groups of how many? Of course, it's a little different, in that you aren't counting the number of things, but instead the time period over which they happen.

  • Is the first thing special?

    Sometimes, you want to group things over time, except you want to emit the first thing right away. Or, to generalize, perhaps you want a different time interval when the debouncer newly becomes "unsettled" (perhaps a time interval of 0) than when it is already collecting input.

  • Debouncing vs. throttling.

    Do we want to wait for the inputs to "settle" before emitting anyhing, or should we emit on an interval while the debouncer is "in progress."

Considering these three questions together, there seem to be three intervals that are relevant:

  • When "settled" and we receive a new input, what time interval do we wait before first emitting something (possibly Just 0, if we want to emit the input immediately when becoming unsettled, and possibly Nothing if we don't want to treat the first input specially).

  • When "in progress", how long do we wait with no inputs until we should consider ourselves "settled" again?

  • When "in progress", what's the maximum time we should wait before emitting what we've got so far (possibly forever, if we don't want to emit anything until we're settled).

How do we combine things?

For things which happen within the relevant interval, how should they be combined? The "naive" approach to this would be to provide just the last thing, or just the first thing, or just the first and last. But that's not how groupsOf would work for lists -- it would give you all the things, and you could decide which you want.

Now, that would potentially hang onto a lot of interim data that you're just going to throw away. So, even better would be to let you provide your own "folding" function, to decide how the inputs get combined. (I got this idea from the Haskell fold-debounce package).

What are the things?

So, what are these "things" we grouping over time? In Elm, the basic candidates are tasks or messages, since those are the things that happen at some particular time. In a way, you could implement one in terms of the other, since you can construct a Task to send a message, and you can use a message to perform a Task.

One difficulty with working with tasks as the "things" is that once you've constructed a Task, you can't really know anything about it. So, in a way, it's more convenient to accumulate messages than tasks (since messages are just data).

But really, it would be best for some layer of the debouncing to be indifferent as to what the "things" are. It is merely provided the "things" at various times, and emits them, grouped in some way, at other times, whatever they are.

How does one "provide" and "emit" things?

It will be apparent that a debouncer needs to keep some state, and schedule some events. There are some Elm debouncers that use effects modules to do this.

This is convenient because the debouncer can manage its own state -- you don't have to explicitly integrate the debouncer into your model, msg, and update function. However, you do have to supply a global "key" with your debounce requests, to distinguish one debouncer from another. This is awkward -- it seems nicer to avoid the need for globally distinct strings, by having the client code explicitly manage the state.

So, it seems best to provide things to a debouncer by:

  • including the debouncer in your model;
  • delegating its messasges via your update function.

As far as emitting things goes, there would be a number of options. You could imagine an extra parameter returned from an update function (meaning: I've just emitted something -- here it is -- do something with it). Or, you could imagine requiring the client of the package to provide a "mapping" function, so that the package can emit the client's own Msg type. The former seems more flexible, since it would not be difficult for the client to do its own mapping.

API

For the detailed API, see the Elm package site, or the links to the right, if you're already there.

Installation

Try elm-package install Gizra/elm-debouncer

Development

Try something like:

git clone https://github.com/Gizra/elm-debouncer
cd elm-debouncer
npm install
npm test