clojure 2025-10-24

Hi. I was wondering if anyone here can approve/merge a zookeeper-clj PR to bump a dependency to resolve a security finding? https://github.com/liebke/zookeeper-clj/pull/45 Thanks!!

I don't think the owner of that repo is active here? I see you asked about PR 44 about a year ago, but he did review and merge that within a few days...

Thanks. I've waited a few days on the PR and thought I would post here again. Couldn't remember if the nudge in slack helped or not. 😃

you should be able to easily override this in your own project as well

Can I redefine print-method on a specific object via metadata? I have a massive object that is part of a system that I have no control over that is an argument to a function. This object gets picked up on my data when trying to print the rest of the data. The underlying print mechanism for the object blows up. One solution I can think of that's pretty easy is to define a defrecord like (defrecord MyWrapper []) and just pour the object into the record then, like so:

(defrecord MyWrapper [])

(defmethod print-method MyWrapper [v ^java.io.Writer w]
  (.write w "I'm wrapping!!!"))

(into (->MyWrapper) hideous-object)
=> I'm wrapping!!!
Is there a cleaner way using with-meta or similar?

(println (with-meta {} {`clojure.core/print-method (fn [v ^java.io.Writer w]
                                                     (.write w "I'm wrapping!!!"))}))
{}
=> nil ;; Not the effect I was shooting for.
Is the :extend-via-metadata option available for print-method? Seems like to do so would require a protocol with :extend-via-metadata true enabled.

print-method happens to dispatch on type which looks at the :type meta, so you don't even need a defrecord:

(type ^{:type :no-record/needed} {})
;=> :no-record/needed

➕ 1

(defmethod print-method :no-record/needed [v w]
  (.write w "hello"))

(pr-str ^{:type :no-record/needed} {})
;=> "hello"

Ah, I see. Is there way to inline the print-method like I did above?

hmm, not sure why you'd need that - just put whatever you'd want to print into the metadata (using with-meta), and then extract it with the method?

Yeah, that makes sense. Thanks!

Is there a good way to write a macro that maintains some shared state across every invocation of that particular code? For instance, imagine I wanted (caching (prn :hi) 2) , where caching evaluates its body once, and thereafter simply returns 2.

FUN FACT: (bound? var) goes through hugely expensive varags seq machinery every call. O___O

👀 1

you can just call the .isBound method directly

Here's a one-liner that does it: (defmacro defn-memoized "Like defn`, but produces a memoized function"` [name args & body] (def name (memoize (fn args ~@body))))` Somewhat more complicated version https://github.com/hyperphor/multitool/blob/59e1736ed3f397c5b13cf3c9bb609da4adfd57b7/src/cljc/hyperphor/multitool/core.cljc#L57.

This is something I use at times

(defmacro static [& body]
  (let [var_ (with-meta (symbol (str "-static-" (java.util.UUID/randomUUID)))
               {:no-doc true})]
    (symbol (intern *ns* var_ (eval `(do ~@body))))))
Perhaps something like
(defmacro caching [& body]
  `(deref (static (delay ~@body)))))
suits your case? Unless you want to eval in the current environment which seems like an... interesting thing to do, though not a particularly difficult one per-se:
(defmacro caching-in-env [& body]
  `(let [p# (static (promise))]
     (if (realized? p#)
       (deref p#)
       (deliver p# (do ~@body)))))

Obviously you can't do

(defmacro caching [& body]
  `(let [cache (promise)] ...))
because the cache would be created afresh on every call to that form. I could def a single cache and update it at macro-expansion time, but I'm a little worried about what might happen with AOT compilation. What about defining a new var at macro-expand time?
(defmacro caching [& body]
  (let [cache-sym (gensym 'cache)]
    (eval `(def ~cache-sym (promise)))
    ... use cache-sym in expanded code))

you want something like an inline cache

Exactly, yeah. Like what the compiler emits for protocol callsites.

This caching example is a little contrived--the actual problem is that I'm writing something like cond that, on the first call, can build a data structure to speed up future calls.

I don't think there is much you can do in userspace short of squirt out custom bytecode, but you can abuse the one mechanism that clojure itself uses for a PIC, which is the methodImplCache used by protocols

a volatile field on all functions

something like this?

(let [cache (atom {})]
  (defmacro caching [& args]
    `(let [args# '[~@args]]
       (if-let [ret# (@~cache args#)]
         ret#
         (let [ret# (do ~args)]
           (swap! ~cache args# ret#)
           ret#)))))

My sense is that won't work because cache is no longer in scope after macro expansion

@~cache will expand to code like (deref <#Atom ....>) right?

i have something like this with a standard def instead of in a let-bind, so that version at least works

Yeah, def should work because the def scope is global

(def cache (atom {}))

(defmacro caching [& args]
  `(let [args# '[~@args]]
     (if-let [ret# (@~cache args#)]
       ret#
       (let [ret# (do ~args)]
         (swap! ~cache args# ret#)
         ret#))))

but then users can touch the cache

Yeah. That's fine, I'm OK showing users engine parts and saying "don't touch"

Not sure if I understand the problem correctly. Do you ask for something like this?

(def cache nil)

(defmacro caching [first-call then]
  (if (nil? cache)
    (do (def cache true)
        first-call)
    then))

(caching (prn :hi) 2) ; prints :hi
(caching (prn :hi) 2) ; returns 2

❓ 1

I think spectre does something like this

➕ 1

Would emitting `(let [cache# ...] (def ..). change anything?

@gtrak That was actually my first approach, but it doesn't work, because every time control enters the macro's generated code, it generates a fresh cache--which is always empty.

If I were writing something like (defcached ...) that might work, but since this macro is supposed to work anywhere, lexically, it can't pull that trick of expanding to a def

Here's what I wound up with: on macro-expansion, (def cache ...), then expanding to code that uses that cache. https://gist.github.com/aphyr/4a85322d7e1fe0092e4e7bf621e7fd34

the eval happening at macro expand time might do weird things with aot

what you want(want is doing a lot here) to do is generate a class with a static field at macro expand time and then write to disk when *compile-files* and then use that static field for your state

(defn define-class-fn [type-name]
  (let [type-name2 (.replaceAll (name type-name) "\\." "/")
        cw (doto (clojure.asm.ClassWriter. 0)
             (.visit clojure.asm.Opcodes/V1_8
                     (+ clojure.asm.Opcodes/ACC_PUBLIC
                        clojure.asm.Opcodes/ACC_SUPER)
                     (str type-name2)
                     nil
                     "java/lang/Object"
                     nil)
             (.visitField (+ clojure.asm.Opcodes/ACC_PUBLIC
                             clojure.asm.Opcodes/ACC_STATIC)
                          "STATE"
                          "Ljava/lang/Object;"
                          nil
                          nil)
             (.visitEnd))
        dcl @clojure.lang.Compiler/LOADER
        bytes (.toByteArray cw)]
    (.defineClass dcl (name type-name) bytes (+ 1 2))
    bytes))

(defmacro with-ic [& body]
  (let [klass-name (symbol (str (munge (name (ns-name *ns*)))
                                "."
                                (gensym 'IC)))
        bytes (define-class-fn klass-name)]
    (when *compile-files*
      (clojure.lang.Compiler/writeClassFile (.replaceAll (name klass-name) "\\." "/") bytes))
    `(let [~'SET (fn [value#] (set! ~(symbol (name klass-name) "STATE") value#))
           ~'GET (fn [] ~(symbol (name klass-name) "STATE"))]
       ~@body)))

user=> (defn foo [] (with-ic (when-not (GET) (SET 0)) (SET (inc (GET))) (GET)))
#'user/foo
user=> (foo)
1
user=> (foo)
2
user=> (foo)
3
user=> (foo)
4
user=> 

depending you may want to make the field volatile or something

Oh wild

I think this should hopefully be OK with AOT, but... I'll find out

you can maybe cheat with eval and quoting but not sure that's a good idea

my guess is with aot the var gets created, but not initialized

but it is just a guess

nods thank you

Well what do you know: java.lang.ClassCastException: class clojure.lang.Var$Unbound cannot be cast to class jepsen.random.WeightedBranchCache

OK, here's another variant. Trying to avoid going all the way to ASM for this; this version checks (bound? cache-var) and uses that for first-time initialization. https://github.com/jepsen-io/jepsen/blob/main/generator/src/jepsen/random.clj#L409-L465

That actually does work in both normal and AOT contexts

Isn't that the use case for memoize?

Sort of. Memoize returns a function, and Delay returns a Delay object, and the caller is expected to store and use the return value of those macros. I want the cache to be implicit, not explicit.