Skip to content

Instantly share code, notes, and snippets.

@swannodette
Last active March 15, 2023 17:38
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save swannodette/5888989 to your computer and use it in GitHub Desktop.
Save swannodette/5888989 to your computer and use it in GitHub Desktop.
(defn debounce
([c ms] (debounce (chan) c ms))
([c' c ms]
(go
(loop [start nil loc (<! c)]
(if (nil? start)
(do
(>! c' loc)
(recur (js/Date.) nil))
(let [loc (<! c)]
(if (>= (- (js/Date.) start) ms)
(recur nil loc)
(recur (js/Date.) loc))))))
c'))
// http://stackoverflow.com/questions/13320015/how-to-write-a-debounce-service-in-angularjs
app.factory('debounce', function($timeout, $q) {
return function(func, wait, immediate) {
var timeout;
var deferred = $q.defer();
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if(!immediate) {
deferred.resolve(func.apply(context, args));
deferred = $q.defer();
}
};
var callNow = immediate && !timeout;
if ( timeout ) {
$timeout.cancel(timeout);
}
timeout = $timeout(later, wait);
if (callNow) {
deferred.resolve(func.apply(context,args));
deferred = $q.defer();
}
return deferred.promise;
};
};
});
@overthink
Copy link

Does that first recur (recur (js/Date.)) have the correct arity for the enclosing loop? I'm confused by that part.

@dball
Copy link

dball commented Jun 29, 2013

I don't see how that works either. Clojure says, "the recur expression must match the arity of the recursion point exactly".

@ghoseb
Copy link

ghoseb commented Jun 29, 2013

Looks like a typo. Line #8 should look exactly like line #12

@swannodette
Copy link
Author

@overthink @dball @ghoseb, thanks for the review, yes that's a typo.

@halgari
Copy link

halgari commented Jun 29, 2013

something to consider: pass c' in as an argument, or provide a different arity that gives you your desired default. In the JCSP docs and in my own tests, you often build sets of channels then want to wire them together, having all constructs take channels as arguments allows you to write stuff up after the channels have already been created.

@swannodette
Copy link
Author

@halgari thanks for the review excellent points

@madvas
Copy link

madvas commented Aug 28, 2015

I found given implementation of debounce incorrect. The point of debounce is that it gets fired exactly once after all frequent calls end. This implementation doesn't put into channel when all calls end.
This demo of debounce is correct: Ben Alman » jQuery throttle / debounce » Examples » Debounce

So I wrote bit different debounce which I think is correct

Simple version:

(defn debounce
  ([c ms] (debounce (chan) c ms))
  ([c' c ms]
   (go
     (loop [timeout nil]
       (let [loc (<! c)]
         (when timeout
           (js/clearTimeout timeout))
         (let [t (js/setTimeout #(go (>! c' loc)) ms)]
           (recur t)))))
   c'))

Version with immediate param. This should be equivalent to above AngularJS snippet. When immediate true, first call always gets fired instantly and only after that debouncing starts. It might be desired in some cases.

(defn debounce
  ([c ms] (debounce (chan) c ms false))
  ([c ms immediate] (debounce (chan) c ms immediate))
  ([c' c ms immediate]
   (go
     (loop [start (js/Date.) timeout nil]
       (let [loc (<! c)]
         (when timeout
           (js/clearTimeout timeout))
         (let [diff (- (js/Date.) start)
               delay (if (and immediate
                              (or (>= diff ms)
                                  (not timeout)))
                       0 ms)
               t (js/setTimeout #(go (>! c' loc)) delay)]
           (recur (js/Date.) t)))))
   c'))

@aiba
Copy link

aiba commented Apr 18, 2016

See also https://gist.github.com/scttnlsn/9744501 for a version of debounce that outputs the final input after a flurry of inputs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment