19

How can you get function's name as string in Clojure?

What I have so far doesn't look anywhere near idiomatic:

(defn fn-name
  [f]
  (first (re-find #"(?<=\$)([^@]+)(?=@)" (str f))))

(defn foo [])

(fn-name foo) ;; returns "foo"

EDIT: With the provided hints I put together a basic macro that does what I want. Does it look better?

(defmacro fn-name
  [f]
  `(-> ~f var meta :name str))
4
  • 3
    What is the point of this macro? It can only work on literals, like (fn-name inc), whereas (let [f inc] (fn-name f)) will fail. If you're typing inc in literally, you can just type "inc" instead and save some characters! Commented Mar 2, 2014 at 5:19
  • 1
    That's its limitation. As @noisesmith mentioned, it works for defns from the same namespace. Typing the function name also as a string isn't an option for me. I'm looking for how to get the name of any function passed as parameter. Commented Mar 2, 2014 at 10:32
  • Do functions as such have names? Commented Mar 2, 2014 at 11:35
  • No. As Sean Devlin (groups.google.com/d/msg/clojure/ORRhWgYd2Dk/kxauUUhQjbsJ) writes, the function object has no name. Only the symbol to which the function is bound has one. I could have made my question clearer. Commented Mar 2, 2014 at 12:04

5 Answers 5

10

For functions defined with the defn form:

user> (-> #'map meta :name)
map
user> (defn nothing [& _])
#'user/nothing
user> (-> #'nothing meta :name)
nothing

This requires access to the var, rather than just the function value the var holds.

Sign up to request clarification or add additional context in comments.

5 Comments

Access to the var is what's limiting in this case, as it means I can't wrap such code in function. However, with your hints (and hints from elsewhere) I created a macro that does the job. See my edited question. What do you think about such solution?
it looks reasonable, as long as you don't have a scenario like (fn [f] (fn-name f)), since the var will be resolved at point of argument evaluation.
Exactly, I'm aware of this and I'm wondering if I can solve this by passing the macro with a "fully-qualified" function name instead.
What is #'map? I know #{} for sets and ' is quote.
#'foo is a reader macro that expands to (var foo) - it accesses the mutable var owned by the namespace, rather than the value stored in it. Generally # introduces reader macros #() for functions, #{} for sets, #inst for date, etc.
5

EDIT: I found a better way Clojure includes a function called demunge. So instead of re-inventing the wheel, just use it ;)

(clojure.repl/demunge (str map?));; "clojure.core/map?@2b68895c"

Or if you want a prettified version

(defn- pretty-demunge
  [fn-object]
  (let [dem-fn (demunge (str fn-object))
        pretty (second (re-find #"(.*?\/.*?)[\-\-|@].*" dem-fn))]
    (if pretty pretty dem-fn)))

(pretty-demunge map?);; "clojure.core/map?"

I think there is a more clean way to do this. I ran into the same problem of wanting to know the name of function gotten as an argument in a function. I couldn't use the macro because I needed to map it so here is what I've got: (assume string? is the function passed as argument)

(clojure.string/replace (second (re-find #"^.+\$(.+)\@.+$" (str string?)))
                        #"\_QMARK\_" "?")
; string?

Of course this is not a complete solution but I'm sure you can work your way through from here. Basically Clojure mangles the function name into something it can use. So the basic though is obviously: you need to unmangle that ! Which is pretty easy since str works on any function :D, returning the mangled name of the function.

By the way, this also works

(def foo string?)
(clojure.string/replace (second (re-find #"^.+\$(.+)\@.+$" (str foo)))
                        #"\_QMARK\_" "?")
; string?

Have fun

2 Comments

Oh I just realized that this is almost the same thing that you had at the beginning. I don't understand though what is the problem with it. It looks perfectly fine for me.
The regex #"(.*?\/.*?)[\-\-|@].*" stops at the first -. For instance, (pretty-demunge assoc-in) returns clojure.core/assoc. If you just get everything before the @, I think the main edge case is when the string ends with #"--[0-9]+" (which is caused by lambdas, such as when a function was defined by (def my-fn (fn ...))). So you'll need to check for two possibilities: (last (or (re-find #"(.+)--\d+@" dem-fn) (re-find #"(.+)@" dem-fn))).
3

I suggested an improvement to @carocad's answer in a comment. Since I also needed to do this, and I needed to do it in both clj and cljs, here is what I came up with:

(ns my-ns.core
  (:require [clojure.string :as s]
            #?(:clj [clojure.main :refer [demunge]])))

(defn fn-name
  [f]
  #?(:clj
      (as-> (str f) $
            (demunge $)
            (or (re-find #"(.+)--\d+@" $)
                (re-find #"(.+)@" $))
            (last $))
     :cljs
      (as-> (.-name f) $
            (demunge $)
            (s/split $ #"/")
            ((juxt butlast last) $)
            (update $ 0 #(s/join "." %))
            (s/join "/" $))))

Note that cljs.core has its own demunge built in and can't access clojure.main.


Edit

Note that when you do (with-meta a-fn {...}), it returns a clojure.lang.AFunction in clojure, which hides the underlying name and namespace information. There may be other situations like that, I'm not sure. With fn literal forms, you can do ^{...} (fn ...) instead of (with-meta (fn ...) {...}) and it won't return a clojure.lang.AFunction, but that workaround won't work with predefined functions. I haven't tested any of this in clojurescript to see if it works the same way.

Note also that anonymous functions in clojure will always end in fn, as in "my-ns.core/fn". Normally these functions would have a "--[0-9]+" at the end, but the regex above removes that. You could modify the function above and make an exception for anonymous functions. As long as the lambda has an internal name, that will be used, e.g.:

(fn-name (fn abc [x] x)) ;;=> "my-ns.core/abc"

Again, I haven't tested any of these notes in clojurescript yet.

Comments

0

What I'm currently doing to extract function names from backtraces is as follows:

(defn remove-anon
  "Remove anonomous function elements from a `munged` name."
  [^String munged]
  (remove #(starts-with? % "fn__") 
          (split munged #"[\.\$]")))

(defn fn-name
  "De-mung a `munged` function name"
  [^String munged]
  (replace (last (remove-anon munged)) "_" "-"))

(defn recover-function-name
  [^StackTraceElement frame]
  (when (ends-with? (.getFileName frame) ".clj")
    (fn-name (.getClassName frame))))

This seems to work. I appreciate that it isn't exactly what you asked, but may be useful.

EDITED

But this is cleaner and works equally well:

(defn remove-anon
  "Remove anonomous function elements from a `munged` name."
  [^String munged]
  (remove #(starts-with? % "fn__")
          (split munged #"\$")))

(defn fn-name
  "De-mung a `munged` function name"
  [^String munged]
  (when (re-find #"\$" munged)
    (replace (last (remove-anon munged)) "_" "-")))

(defn recover-function-name
  [^StackTraceElement frame]
  (when (ends-with? (.getFileName frame) ".clj")
    (fn-name (.getClassName frame))))

Comments

0

In clojurescript I use the following:

(defn fn-name [f]
  (let [s (str f)
        s (subs s 9 (clojure.string/index-of s "("))]
    (second (re-matches #"^.*\$([^\$]+)$" s))))

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.