How to print a source of a function passed as a parameter

Let’s say I have a function that takes another function as an argument. I’d like to print the source of the function passed. i.e.:

(defn foo [f]
  (print
  ;; need to know the source of 'f' here
  ))

Is that possible? I tried using clojure.repl/source and clojure.repl/source-fn and clojure.core/meta but I can’t seem to find a way.

Not possible if you pass in just the regular function since that will be a Function instance. You can get the JS source but I presume you want the CLJS source?

You could modify the place where you call it to pass addtional information so you can eventually get the source info but a regular passed function does not have the required info to get the source.

In this specific case I need to know the source of Clojure function, I suppose whatever solution is out there, it would be very different for Clojurescript.

It’s totally hacky but something like this could work:

(defn var-source
  "This is almost an exact copy of `clojure.repl/source-fn` but
  accepting var instead of symbol in current ns (*ns*)."
  [v]
  (when-let [filepath (:file (meta v))]
    (when-let [strm (.getResourceAsStream (clojure.lang.RT/baseLoader) filepath)]
      (with-open [rdr (java.io.LineNumberReader. (java.io.InputStreamReader. strm))]
        (dotimes [_ (dec (:line (meta v)))] (.readLine rdr))
        (let [text (StringBuilder.)
              pbr (proxy [java.io.PushbackReader] [rdr]
                    (read [] (let [i (proxy-super read)]
                               (.append text (char i))
                               i)))
              read-opts (if (.endsWith ^String filepath "cljc") {:read-cond :allow} {})]
          (if (= :unknown *read-eval*)
            (throw (IllegalStateException. "Unable to read source while *read-eval* is :unknown."))
            (read read-opts (java.io.PushbackReader. pbr)))
          (str text))))))


(defn fn-source
  [f]
  (let [[ns-name sym-name]
        (-> (str f)
            (clojure.repl/demunge)
            (clojure.string/replace #"@.*$" "")
            (clojure.string/split #"/"))
        ns (the-ns (symbol ns-name))
        fn-var (ns-resolve ns (symbol sym-name))]
    (var-source fn-var)))

(comment
  (fn-source tap>)
;; => "(defn tap>\n  \"sends x to any taps. Will not block. Returns true if there was room in the queue,\n  false if not (dropped).\"\n  {:added \"1.10\"}\n  [x]\n  (force tap-loop)\n  (.offer tapq (if (nil? x) ::tap-nil x)))"
  )

1 Like
(require '[clojure.repl :refer [demunge source-fn]])

(defn fn-source [f]
  (let [fn-name (-> f .getClass .getName)
        fn-name (demunge fn-name)
        fn-sym (symbol fn-name)]
    (println (source-fn fn-sym))))

(fn-source +)
;; =>
;; (defn +
;;   "Returns the sum of nums. (+) returns 0. Does not auto-promote
;;   longs, will throw on overflow. See also: +'"
;;   {:inline (nary-inline 'add 'unchecked_add)
;;    :inline-arities >1?
;;    :added "1.2"}
;;   ([] 0)
;;   ([x] (cast Number x))
;;   ([x y] (. clojure.lang.Numbers (add x y)))
;;   ([x y & more]
;;    (reduce1 + (+ x y) more)))
8 Likes

@bartuka That code doesn’t fit the requirements:

(require '[clojure.repl :refer [source-fn]])

(defmacro sym->source [sym]
  (println (source-fn sym)))

(defn foo [f]
  (sym->source f))

(foo +) ;;=> nil
1 Like

Oh, that’s true. It works only by passing the f directly. I will delete the answer as it is documented in your response as well. thanks @borkdude !!

For those wondering, as I did, what the difference between clojure.repl/source and clojure.repl/source-fn is, source is a macro that prints the source code of a given function directly to standard out, and actually uses source-fn, whereas source-fn is a function that returns a string representation of the given function.

Both require that the function be given as a symbol that resolves to a Var defined in a namespace for which the .clj is in the classpath. This is why it is necessary to first get the name of the function, demunge it (e.g. (clojure.repl/demunge "clojure.core$_PLUS_") => "clojure.core/+"), and then convert it to a symbol before passing it as an argument as @borkdude showed in his answer.

It works for named functions. But there’s no way to get the source of inline (lambda) function, right?