3

In clojure, can one idiomatically obtain a function's name inside of its body, hopefully accomplishing so without introducing a new wrapper for the function's definition? can one also access the function's name inside of the body of the function's :test attribute as well?

For motivation, this can be helpful for certain logging situations, as well as for keeping the body of :test oblivious to changes to the name of the function which it is supplied for.

A short elucidation of the closest that meta gets follows; there's no this notion to supply to meta, as far as I know, in clojure.

(defn a [] (:name (meta (var a))))

Obviously it is easy to accomplish with a wrapper macro.

Edit: luckily no one so far mentioned lambda combinators.

matanster
  • 15,072
  • 19
  • 88
  • 167

1 Answers1

3

There are 2 ways to approach your question. However, I suspect that to fully automate what you want to do, you would need to define your own custom defn replacement/wrapper.


The first thing to realize is that all functions are anonymous. When we type:

(defn hello [] (println "hi"))

we are really typing:

(def hello (fn [] (println "hi"))

we are creating a symbol hello that points to an anonymous var which in turn points to an anonymous function. However, we can give the function an "internal name" like so:

(def hello (fn fn-hello [] (println "hi")))

So now we can access the function from the outside via hello or from the inside using either hello of fn-hello symbols (please don't ever use hello in both locations or you create a lot of confusion...even though it is legal).

I frequently use the fn-hello method in (otherwise) anonymous functions since any exceptions thrown will include the fn-hello symbol which makes tracking down the source of the problem much easier (the line number of the error is often missing from the stack trace). For example when using Instaparse we need a map of anonymous transform functions like:

   {
     :identifier     fn-identifier
     :string         fn-string
     :integer        (fn fn-integer [arg] [:integer (java.lang.Integer. arg)])
     :boolean        (fn fn-boolean [arg] [:boolean (java.lang.Boolean. arg)])
     :namespace      (fn fn-namespace [arg] [:namespace arg])
     :prefix         (fn fn-prefix [arg] [:prefix arg])
     :organization   (fn fn-organization [arg] [:organization arg])
     :contact        (fn fn-contact [arg] [:contact arg])
     :description    (fn fn-description [arg] [:description arg])
     :presence       (fn fn-presence [arg] [:presence arg])
     :revision       (fn fn-revision [& args] (prepend :revision args))
     :iso-date       (fn fn-iso-date [& args] [:iso-date (str/join args)])
     :reference      (fn fn-reference [arg] [:reference arg])
     :identity       (fn fn-identity [& args] (prepend :identity args))
     :typedef        (fn fn-typedef [& args] (prepend :typedef args))
     :container      (fn fn-container [& args] (prepend :container args))
     :rpc             (fn fn-rpc [& args] (prepend :rpc args))
     :input           (fn fn-input [& args] (prepend :input args))
...<snip>...
     } 

and giving each function the "internal name" makes debugging much, much easier. Perhaps this would be unnecessary if Clojure had better error messages, but that is a longstanding (& so far unfullfilled) wish.

You can find more details here: https://clojure.org/reference/special_forms#fn

If you read closely, it claims that (defn foo [x] ...) expands into

(def foo (fn foo [x] ...))

although you may need to experiment to see if this has already solved the use-case you are seeking. It works either way as seen in this example where we explicitly avoid the inner fn-fact name:

(def fact (fn [x] ; fn-fact omitted here
            (if (zero? x) 
              1 
              (* x (fact (dec x))))))

(fact 4) => 24

This version also works:

(def fact (fn fn-fact [x] 
            (if (zero? x) 
              1 
              (* x (fn-fact (dec x))))))

(fact 4)     => 24
(fn-fact 4)  => Unable to resolve symbol: fn-fact 

So we see that the "internal name" fn-fact is hidden inside the function and is invisible from the outside.


A 2nd approach, if using a macro, is to use the &form global data to access the line number from the source code. In the Tupelo library this technique is used to improve error messages for the

(defmacro dotest [& body] ; #todo README & tests
  (let [test-name-sym (symbol (str "test-line-" (:line (meta &form))))]
    `(clojure.test/deftest ~test-name-sym ~@body)))

This convenience macro allows the use of unit tests like:

(dotest
  (is (= 3 (inc 2))))

which evalutes to

(deftest test-line-123   ; assuming this is on line 123 in source file
  (is (= 3 (inc 2))))

instead of manually typing

(deftest t-addition
  (is (= 3 (inc 2))))

You can access (:line (meta &form)) and other information in any macro which can make your error messages and/or Exceptions much more informative to the poor reader trying to debug a problem.

Besides the above macro wrapper example, another (more involved) example of the same technique can be seen in the Plumatic Schema library, where they wrap clojure.core/defn with an extended version.


You may also wish to view this question for clarification on how Clojure uses the "anonymous" var as an intermediary between a symbol and a function: When to use a Var instead of a function?

Community
  • 1
  • 1
Alan Thompson
  • 29,276
  • 6
  • 41
  • 48
  • 1
    Not that it makes any difference, but `(defn hello [] (println "hi"))` is really `(def hello (fn hello [] (println "hi"))`, since the function is allowed to be recursive. The local name seems to be quite forgotten. – Thumbnail Apr 17 '17 at 23:49
  • @AlanThompson thanks a lot! I've just learnt a lot here beyond just the pointwise question, and this discussion has been really excellent – matanster Apr 18 '17 at 17:54
  • @thumbnail and AlanThompson I don't think the original first couple of comments exchanged add any info over the answer as currently phrased... may I suggest simply dropping (deleting) them now (?) as they are a bit confusing as it currently stands after the answer was updated accordingly, if I've not misinterpreted anything. – matanster Apr 18 '17 at 17:58
  • One comment is that for a macro solution to my original question, I find it would suffice to wrap around `defn` such that the function name is stuffed into the function body by the macro, quite equivalently to the macro example in this answer. `meta` would not even be needed, although that's certainly nice and practical test wrapper inspiration to carry forward! – matanster Apr 18 '17 at 18:17
  • Yes, I think a macro wrapper around `defn` is the best way forward for your goal. See above edits for a link to the Plumatic Schema example. – Alan Thompson Apr 18 '17 at 20:45