Fork me on GitHub
#clojure
<
2021-10-03
>
Carsten Behring21:10:41

Can I redefine "defn", so it stores the form past to it in metadata ?

phronmophobic21:10:50

another option is to use the existing meta to look up the function. There's a built in function clojure.repl/source, but it is a little inconsistent. I adapted source to work more consistently, which you can see at: https://github.com/phronmophobic/reveal-exception/blob/main/src/com/phronemophobic/reveal_plugin/reveal_exception.clj#L83

Carsten Behring21:10:38

yes, I did use that. My use case goes into "diffing" of clojure source code, but per function (instead of being file based) So I thought it could be interesting to look at the form which defined a function.

didibus21:10:51

Yes you can do that if you want

didibus21:10:28

But don't try to re-implement defn, just leverage the existing defn

Carsten Behring21:10:29

how could I do that ?

Carsten Behring21:10:18

without getting in a circle ?

phronmophobic22:10:24

are you diffing between versions in version control or redefined at the repl?

Carsten Behring22:10:38

hmmm. I want to track changes in the definition of functions over time. They are defined in source files. So the source-fn approach works in principle.

Carsten Behring22:10:53

Just a pitty that it gives me a String back.

Carsten Behring22:10:15

So I read somewhere te idea to change the defn macro, to store the form as metdata.

phronmophobic22:10:20

it's pretty easy to get a data structure from the string, you can just use read

phronmophobic22:10:28

or read-string

Carsten Behring22:10:43

Lets give it a try.

phronmophobic22:10:29

are you tracking changes while someone is working at the repl or some other way?

phronmophobic22:10:33

if you're trying to track changes at the repl, I might go the route of writing some repl middleware that receives each expression that will be eval 'd

phronmophobic22:10:06

if you're tracking changes in source files, I would use a tool like rewrite-clj that can parse clojure source into an AST

Carsten Behring22:10:16

the source code -> read-string is fine,., probbaly

Carsten Behring22:10:31

and the file parsing is as well a good idea.

phronmophobic22:10:26

also check out https://clojure-lsp.io/ which has great static analysis tools that might be helpful

didibus22:10:42

You can call the core one with full qualification: clojure.core/defn

didibus22:10:06

(defmacro defn
  [& body]
  `(doto
       (clojure.core/defn [email protected])
     (alter-meta!
      merge
      {:form '~&form})))

didibus22:10:31

That will put the full source of the defn on the meta key named :form

didibus22:10:24

You can try it with:

(defn foobar
  "This is a doc"
  {:some-attr :a1}
  ([] (foobar 1 2 3))
  ([a b c]
   {:pre [(number? a) (number? b) (number? c)]}
   (+ a b c))
  {:some-other-attribute :a2})

(:form (meta #'foobar))

didibus22:10:37

It preserves all features of defn, and it should work even when AOT compiled I believe.

Carsten Behring21:10:09

I got a good start with doing this:

(defmacro defn* [x & body]
  (let [form `'~&form
        x    (vary-meta x assoc :form form)]
    `(def  ~x (fn  [email protected]))))
(intern 'clojure.core
        (with-meta 'defn {:macro true})
        @#'defn*)

(defn sub [a b] (- a b))

(:form
 (meta #'sub))
;; => (defn sub [a b] (- a b))

Carsten Behring21:10:51

and it works for simple "defn". But require a more complex ns after this fails.

Carsten Behring21:10:38

probably my defn* macro is too simple to work with all types of defn invocations in real code.

didibus22:10:23

To substitute it is a bit tricker. You need to grab the Var of the real defn and save it somewhere. Then ns-unmap from clojure.core defn, and then def inside clojure.core your new defn, and switch your defn to call the var you saved of the original. I think that should work, though I've never tried to substitute a core macro that also called the original, I only did it with functions which are a bit simpler cause you can swap the var to point to the new function.