Rate-limiting a Signal

169 views
Skip to first unread message

Richard Feldman

unread,
Nov 6, 2014, 1:19:06 PM11/6/14
to elm-d...@googlegroups.com
I came across an interesting use case. I can definitely implement it using ports, but I'm trying to think of a way to do it just using Signals without involving ports.

Basically I have a text box which is used to search for things. As the user types, a keyup input signals an outbound port, which takes care of performing the search.

The trick is, I don't want to perform a new search on every single keystroke. Instead, I want to wait until the user is "done typing" (which I'll define as "it's been at least 500ms since the last keyup") before sending the signal to the outbound port to perform the search.

In imperative terms, I need to go from this:

Keyup event -> search
Keyup event -> search
Keyup event -> search

...to this:

Keyup event -> wait to see if we go 500ms before another keyup event (and since we do end up getting another one, this keyup will have no effect)
Keyup event -> again start waiting to see if we go 500ms without another keyup event (and again we get another one, so this keyup will likewise have no effect)
Keyup event -> wait 500ms, don't see any more keyup events, so go ahead and perform the search

In Elm terms, I want to translate the keyup input into a Signal that incorporates this rate-limiting logic.

Anyone have thoughts on how best to go about doing this?

Paul Chiusano

unread,
Nov 6, 2014, 1:41:14 PM11/6/14
to elm-d...@googlegroups.com
I think there may have been a thread about this recently. I have the following function defined, I forget whether I wrote it or got it from someone on this list. I would not be surprised if it was Jeff, who schooled me with some minority report level stuff in a recent thread. :)

{-| Only emit updates of `s` when it settles into a steady state with
    no updates within the period `t`. Useful to avoid propagating updates
    when a value is changing too rapidly. -}
steady : Time -> Signal a -> Signal a
steady t s = sampleOn (since t s |> dropRepeats) s

Paul :)

Richard Feldman

unread,
Nov 6, 2014, 2:44:32 PM11/6/14
to elm-d...@googlegroups.com
Interesting, thanks! I'm not quite following one part of this. I renamed some things to help myself out:

steadyFor : Time -> Signal a -> Signal a
steadyFor time signal = sampleOn (dropRepeats (since time signal)) signal

That inner bit has me confused:

dropRepeats (since time signal)

"since" returns Signal Boolean, which I take to mean that dropRepeats will drop a bunch of False events and then keep the lone True event that comes after the specified time has elapsed after the keyup signal.

But what if the user pressed one key, then waited longer than 500ms, then pressed another key? If I understand "since" correctly, its resulting Signal Bool will fire exactly two events in that case, and they will both be True, so dropRepeats will drop them as a repeat, and the search will fail to be performed even though it should.

The docs for "since" say its Signal's value is False by default, but I assume it's not sending out False events every millisecond, right? I assume it would only actually fire (thereby passing through dropRepeats) when it has a True to report (although I admit that in that case I don't know under what circumstances the False would come up).

What am I missing there? 

--
You received this message because you are subscribed to a topic in the Google Groups "Elm Discuss" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/elm-discuss/2CHk-bE0Gpw/unsubscribe.
To unsubscribe from this group and all its topics, send an email to elm-discuss+unsubscribe@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Janis Voigtländer

unread,
Nov 6, 2014, 3:09:18 PM11/6/14
to elm-d...@googlegroups.com
Of the two True, dropRepeats will only drop the first, not both.

--
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 elm-discuss...@googlegroups.com.

Paul Chiusano

unread,
Nov 6, 2014, 3:20:05 PM11/6/14
to elm-d...@googlegroups.com
You know, I'm not sure I ever properly tested that function. I went nuts implementing various Signal combinators just to get a sense of what was expressible. Does it not work as you expect?

I am wondering if instead of `dropRepeats (since time signal)` it should be `dropIf identity False (since time signal)`. So, we ignore all `True` events and are only catching when the signal switches from `True` back to `False`. The `dropRepeats` version seems like it will generate an event at both the start and end of the `time` window after each firing of `signal`.

Paul

To unsubscribe from this group and all its topics, send an email to elm-discuss...@googlegroups.com.

Max Goldstein

unread,
Nov 6, 2014, 10:22:26 PM11/6/14
to elm-d...@googlegroups.com
The term you are looking for is debounce. You can see a JS implementation as part of underscore. I haven't really thought about it, but my guess is that there is no simple way to debounce a signal in Elm today. I support adding it as a primitive. It would have the type signature of steadyFor above.

There is also a similar function throttle, which would also make a fine primitive. It differs from debounce as follows:
  • When a throttled signal has been steady for a long time, and then an event occurs, that event is propagated immediately. Not so for a debounced signal.
  • If a throttled signal receives events frequently, events are still sent periodically. A debounced signal does not produce an event until after events have subsided. debounce second (fps 30) will never produce a value because it never settles for long enough.
While we're considering time-related signal primitives, there is also delayBy. I think this is some of the most interesting parts of Elm's development, because we're creating combinators for an abstraction (signals) that has never been part of a widely-used programming language. That's part of what makes Elm unique -- it handles time-varying values rather than grafting clock time on to logical time (mutation).

Janis Voigtländer

unread,
Nov 7, 2014, 1:36:04 AM11/7/14
to elm-d...@googlegroups.com
In contrast, I think that debounce is very well implementable in Elm today, with exactly the semantics of the JS implementation you linked to, including the possibility to control whether the resulting events should be fired on the leading or trailing edge of the wait interval.

Based on Paul's earlier suggestion, we have:

debounce : Signal a -> Time -> Bool -> Signal a
debounce signal wait immediate = sampleOn
                                   (   since wait signal
                                    |> dropRepeats
                                    |> dropIf (xor immediate) False)
                                   signal

which implements the behavior described in http://underscorejs.org/#debounce.


--
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 elm-discuss...@googlegroups.com.

Janis Voigtländer

unread,
Nov 7, 2014, 1:41:04 AM11/7/14
to elm-d...@googlegroups.com
Of course, that statement of mine was nonsense. The function will drop the second, not the first occurrence of True. The point is, it will not drop both. That is why the debounce implementation in the other message will indeed work.

Max Goldstein

unread,
Nov 9, 2014, 3:04:51 PM11/9/14
to elm-d...@googlegroups.com
Well, look at that. (Demo)

You need to use (/=) rather than xor, which is for integers (and not semantically correct besides). I understand that you followed the underscore interface to prove a point. But to make it more Elm-like I would (1) make the signal last (2) split it into two functions rather than take a boolean.

Maybe we should make this into a library.

Richard Feldman

unread,
Nov 9, 2014, 3:11:41 PM11/9/14
to elm-d...@googlegroups.com
Awesome! This implementation of debounce works exactly as advertised. :D

I was just about to post about both of the Underscore -> Elm considerations (having the signal be last would fit the Elm API style guidelines, and that 2 functions makes more sense than taking a boolean), but instead I'll +1 your suggestions.

Thanks a ton for the help! This is just what the doctor ordered.

To unsubscribe from this group and all its topics, send an email to elm-discuss...@googlegroups.com.

Janis Voigtländer

unread,
Nov 9, 2014, 3:15:06 PM11/9/14
to elm-d...@googlegroups.com
Hmm, according to the docs, http://library.elm-lang.org/catalog/elm-lang-Elm/0.13/Basics#xor, xor works on Booleans. Also, I have no idea why it would be semantically incorrect. xor on Booleans is the same as /= on Booleans.

--

Richard Feldman

unread,
Nov 9, 2014, 3:18:17 PM11/9/14
to elm-d...@googlegroups.com
Incidentally, the diff on adding this feature this looks really clean and natural: https://github.com/rtfeldman/dreamwriter/commit/02d5210f80485e1bdcf66eaf81c5fd17fa835c04

You received this message because you are subscribed to a topic in the Google Groups "Elm Discuss" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/elm-discuss/2CHk-bE0Gpw/unsubscribe.
To unsubscribe from this group and all its topics, send an email to elm-discuss...@googlegroups.com.

Max Goldstein

unread,
Nov 9, 2014, 8:59:36 PM11/9/14
to elm-d...@googlegroups.com
Hmm, according to the docs, http://library.elm-lang.org/catalog/elm-lang-Elm/0.13/Basics#xor, xor works on Booleans. Also, I have no idea why it would be semantically incorrect. xor on Booleans is the same as /= on Booleans.

I didn't think of Basics. I was thinking of Bitwise: http://library.elm-lang.org/catalog/elm-lang-Elm/0.13/Bitwise#xor

I guess it's six of one, half-dozen of the other. 
Reply all
Reply to author
Forward
0 new messages