Search code examples
clojureclojurescript

A function that creates functions that can be interrogated for the number of times they have been called


I want to create a function that creates other functions that remember how many times they have been called -- not hard. But I want to interrogate those functions at some arbitrary time in the future, not immediately after being called. It might work like this:

(defn mk-cntr []
  (let [count (atom 0)
        res (fn cntr [] (swap! count inc))]
    res))

(let [f (mk-cntr)]
  (f)
  (f)
  ; other complicated stuff here
  (f)
  ; Interrogate it now.
  (println (:count f))) ; or something similar

This does create functions that count how many times they have been called, but I don't want that information right after the call.

I would like to reproduce something like this JavaScript snippet:

function counter() {
  function result() { result.count++ }
  result.count = 0
  return result
}

With this, you can do let cntr = counter() then sometime later interrogate it like cntr.count to find out how many times it was called.

I've fumbled around with function metadata but couldn't come up with something that worked. It feels like defrecord and defprotocol could be used, but that seems like a lot of bother to come up with something that is so simple in JavaScript.

Is there a simple way to implement this is Clojure/Script?


Solution

  • If you have a fixed function with a known number of arguments you want to track you can create a record and implement IFn for the required arity:

    (defrecord FC [counter]
      IFn
      (invoke [this]
        (let [res (swap! counter inc)]
          res)))
    
    (defn mk-cntr []
      (->FC (atom 0)))
    

    If you need to wrap some arbitrary function(s) I would create a record containing the wrapping function and the counter:

    (defn mk-cntr2 [f]
      (let [counter (atom 0)]
        {:counter counter
         :fn (fn [& args]
               (swap! counter inc)
               (apply f args))}))
    

    which you can then call and query:

    (def f (mk-cntr2 println))
    ((:fn f) "first")
    ((:fn f) "second")
    @((:counter f)) ;; 2