clojure

Jake Pearson 2025-10-24T15:37:45.421909Z

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!!

seancorfield 2025-10-24T15:46:42.498279Z

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...

Jake Pearson 2025-10-24T15:48:12.626369Z

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. 😃

dpsutton 2025-10-24T15:51:11.458529Z

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

2025-10-24T17:02:58.795719Z

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?

2025-10-24T17:05:02.902259Z

Ah, it's :extend-via-metadata that I'm thinking of https://clojure.org/reference/protocols#_extend_via_metadata.

2025-10-24T17:10:01.501199Z

(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.

yuhan 2025-10-24T17:10:47.518609Z

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
yuhan 2025-10-24T17:12:55.732109Z

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

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

2025-10-24T17:14:21.361849Z

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

yuhan 2025-10-24T17:17:51.558559Z

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?

2025-10-24T17:20:41.833649Z

Yeah, that makes sense. Thanks!

Kyle Kingsbury 2025-10-24T17:27:40.782729Z

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.

Kyle Kingsbury 2025-10-25T17:24:11.380699Z

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

👀 1
2025-10-25T17:33:19.492519Z

you can just call the .isBound method directly

2025-10-29T17:50:33.932649Z

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.

exitsandman 2025-10-27T16:20:10.766409Z

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)))))

Kyle Kingsbury 2025-10-24T17:31:43.409049Z

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))

ghadi 2025-10-24T17:34:38.710039Z

you want something like an inline cache

Kyle Kingsbury 2025-10-24T17:34:55.687719Z

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

Kyle Kingsbury 2025-10-24T17:36:08.041369Z

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.

ghadi 2025-10-24T17:39:08.281529Z

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

ghadi 2025-10-24T17:40:22.678409Z

a volatile field on all functions

2025-10-24T17:41:39.243519Z

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#)))))

2025-10-24T17:42:38.423629Z

(that's untested)

Kyle Kingsbury 2025-10-24T17:47:20.862429Z

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

Kyle Kingsbury 2025-10-24T17:47:54.698159Z

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

2025-10-24T17:48:05.416859Z

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

Kyle Kingsbury 2025-10-24T17:48:21.017299Z

Yeah, def should work because the def scope is global

2025-10-24T17:48:28.860769Z

(def cache (atom {}))

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

2025-10-24T17:48:35.581619Z

but then users can touch the cache

Kyle Kingsbury 2025-10-24T17:48:51.937869Z

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

hrtmt brng 2025-10-24T18:12:52.019089Z

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
2025-10-24T18:16:09.051099Z

I think spectre does something like this

➕ 1
gtrak 2025-10-24T19:03:27.768629Z

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

Kyle Kingsbury 2025-10-24T19:37:54.866209Z

@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.

Kyle Kingsbury 2025-10-24T19:38:54.740149Z

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

Kyle Kingsbury 2025-10-24T19:45:21.504909Z

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

2025-10-24T20:43:00.324459Z

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

2025-10-24T20:50:37.587059Z

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

2025-10-24T21:12:07.219339Z

(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)))

2025-10-24T21:14:17.527359Z

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=> 

2025-10-24T21:14:28.913929Z

depending you may want to make the field volatile or something

Kyle Kingsbury 2025-10-24T21:24:23.411599Z

Oh wild

Kyle Kingsbury 2025-10-24T21:24:49.847519Z

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

borkdude 2025-10-24T21:25:17.524649Z

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

2025-10-24T21:25:43.777939Z

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

2025-10-24T21:26:07.290819Z

but it is just a guess

Kyle Kingsbury 2025-10-24T21:26:22.834369Z

nods thank you

Kyle Kingsbury 2025-10-24T21:33:54.962239Z

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

Kyle Kingsbury 2025-10-24T22:05:21.697479Z

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

Kyle Kingsbury 2025-10-24T22:05:31.090199Z

That actually does work in both normal and AOT contexts

Ludger Solbach 2025-10-24T17:29:21.658389Z

Isn't that the use case for memoize?

Kyle Kingsbury 2025-10-24T17:33:20.563829Z

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.