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?Ah, it's :extend-via-metadata that I'm thinking of https://clojure.org/reference/protocols#_extend_via_metadata.
(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(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
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#)))))(that's untested)
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 2I think spectre does something like this
https://github.com/redplanetlabs/specter/blob/master/src/clj/com/rpl/specter.cljc#L229-L348
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.