Fork me on GitHub
#clojure
<
2019-02-14
>
gfredericks13:02:28

if anybody wants to have fun with instaparse, regular expressions, and property-based testing, doing a totally-achievable and well-defined thing, you could work on making this code as robust as the clj-jvm version: https://github.com/gfredericks/test.chuck/pull/54

👍 10
Noah Bogart15:02:36

I asked this yesterday afternoon but I don't think I did a good job of describing the question/issue, so I'mma try again but with actual examples this time. I have a macro that accepts an atom (and some other parameters), and binds a lot of helper variables for use in the macro:

(defmacro req [& expr]
  `(fn ~['state 'side 'eid 'card 'targets]
     (let ~['runner '(:runner @state)
            'corp '(:corp @state)
            'run '(:run @state)
            'run-server '(when (:run @state)
                           (get-in @state (concat [:corp :servers] (:server (:run @state)))))
            'servers '(zones->sorted-names (get-zones state))
            etc... ]
        ~@expr)))
This macro is used a lot in the codebase, which means that all of these variables are being bound in every invocation. Most times, only a handful of the let-bound variables are actually used or needed in expr. Is there some way to write the macro such that it will only bind a given variable if that variable is used in expr?

dpsutton15:02:25

its anaphoric and you want to only create the binding if you can find a usage in the body?

Noah Bogart15:02:57

I'm unfamiliar with "anaphoric" in this setting. What do you mean?

dpsutton15:02:20

the macro creates a symbol for the body to use seemingly without a binding creating it

dpsutton15:02:39

the old common lisp ones would introduce a symbol it by convention

Noah Bogart15:02:03

Isn't let a binding?

dpsutton15:02:29

yes but its created at macroexpansion time and therefore not visible in the code you actually write

Noah Bogart15:02:04

Ah yes, that's correct

dpsutton15:02:07

(req (run-server)) is an example. you create the symbol run-server that expr can use without itself creating that binding.

Noah Bogart15:02:48

You are correct, yes

dpsutton15:02:44

i bet @bronsa has an easy way to do this with t.a.jvm. I'm thinking a naive version of walk your expr for the first form might work in practice but would miss things in let bindings, doseq bindings, etc. You don't care if someone shadows since you would create an extra unused binding in a few rare cases. I'm guessing you could run the analyzer over the form, find which things are used and intersect that with the few you have here

Noah Bogart15:02:17

Interesting idea, thanks!

dpsutton15:02:37

its also possible you'll do a lot of work, hit rough edge cases, and after some tedious coding, realize you don't get any measurable performance difference.

Noah Bogart15:02:10

That's what I suspected, which is why I haven't worked very hard at finding a solution, lol

dpsutton16:02:39

(let [ast (ana.jvm/analyze
             '(let [x 3]
                (+ x another-sym something-else)))]
    (into #{}
          (comp (filter (comp #{:var} :op))
                (map :form))
          (ast/nodes ast)))

dpsutton16:02:32

(def another-sym 4)
  (def something-else 5)
  (let [ast (ana.jvm/analyze
             '(let [x 3]
                (+ x another-sym something-else)))]
    (into #{}
          (comp (filter (comp #{:var} :op))
                (map :form))
          (ast/nodes ast)))

  ;;-> #{another-sym something-else}

Noah Bogart16:02:30

That's wild. Thank you!

dpsutton15:02:06

so if i'm reading the AST stuff correctly, you could perhaps figure out how to collect all vars (http://clojure.github.io/tools.analyzer.jvm/spec/quickref.html#var) from the AST of expr and then find which ones are your bindings

dpsutton15:02:38

and tools.analyzer has an ast namespace with a nodes function to give a lazy sequence of nodes in the AST

dpsutton15:02:05

there's also a really cool looking ast->eav function so you could do this in datalog.

dpsutton15:02:22

that's amazing that you can query the AST of clojure code. like that's really cool

bronsa16:02:35

if you want to go down this route, you'd need to collect local bindings that can't be resolved, there's a hook for that

bronsa16:02:09

I'm a bit busy right now but I can show you how to do that later

bronsa16:02:46

I do agree with @dpsutton though that it sounds like it could be just premature optimization

dpsutton16:02:00

> The analyze function can take an environment arg (when not provided it uses the default empty-env) which allows for more advanced usages, like injecting locals from an outer scope: this style?

bronsa16:02:11

mhhhph, that requires knowing in advance the locals you want to inject

bronsa16:02:40

what I was thinking of was to disable the part of the validation pass that throws an error on unresolved symbol names

bronsa16:02:45

then collecting all those nodes

bronsa16:02:00

to figure out which locals are used in the body

dpsutton16:02:15

this is in a macro so i was thinking using &env

bronsa16:02:50

but what you want to look for are locals that are used in expr that don't exist yet

bronsa16:02:58

they won't be in &env

bronsa16:02:04

the context is inverted

bronsa16:02:42

this is what I think is needed

bronsa16:02:21

(require '[clojure.tools.analyzer.jvm :as j])
(require '[clojure.tools.analyzer.ast :as ast])

(defn find-undefined-locals [expr]
 (->> (j/analyze expr
                 (j/empty-env)
                 {:passes-opts (merge j/default-passes-opts
                                      {:validate/unresolvable-symbol-handler
                                       (fn [_ s {:keys [op] :as ast}]
                                         (if (= op :maybe-class)
                                           ast
                                           (throw (Exception. ""))))})})
      ast/nodes
      (filter (fn [{:keys [op]}] (= op :maybe-class)))
      (map :class)))

(find-undefined-locals '(let [a identity] (a b))) ;=> (b)
@dpsutton @nbtheduke

Noah Bogart16:02:51

That's cool as hell, thank you

dpsutton16:02:34

also thanks for this huge amount of work @bronsa

👍 10
bronsa16:02:58

honestly, I haven't used/worked on t.a.jvm in anger in years so I can't remember off the top of my head how to do it, just that it's possible :)

Noah Bogart16:02:44

This is probably an unnecessary optimization (seeing as nearly every binding is either grabbing data from the map atom or performing minor calculations on the atom), but it's currently used 2344 times in the codebase, so it seemed worth at least peeking into

john16:02:12

You could do something like

(defn seq-has? [form namespace-qualified-quoted-sym]
  (when (seq? form)
    (not
      (empty?
        (filter #(= % namespace-qualified-quoted-sym) form)))))

(defn expr-has? [expr namespace-qualified-quoted-sym]
  (when (coll? expr)
    (not
      (empty?
        (->> expr
             (tree-seq coll? seq)
             (filter #(seq-has? % namespace-qualified-quoted-sym)))))))
Adapted from some other code I have, so not tested... (oops, didn't full adapt it... fixed)

bronsa16:02:51

eugh don't do this in macros

bronsa16:02:12

traversing code like this is a recipe for broken edge cases

john16:02:31

Really? Like how?

bronsa16:02:47

it doesn't handle lexical scoping at all

bronsa16:02:20

if you run this over (do a) and (let [a 1] a) it would tell you that both expressions are using the undefined symbol a

bronsa16:02:22

for example

bronsa16:02:39

or even worse, if you ran it over 'a

bronsa16:02:52

which is using the symbol a, not the local a

john16:02:00

Ah, I see

bronsa16:02:06

it would just enter (quote a) ignoring that quote suppresses evaluation

bronsa16:02:19

and so on, dozens of edge cases like this

bronsa16:02:57

there's some constrained cases where this is fine

bronsa16:02:03

like what spec does

bronsa16:02:15

but it's the exception rather than the rule :)

john16:02:30

That makes sense. Won't play nice if somebody intends to (or accidentally) shadow the sym

john16:02:40

Well, I suppose you could fancy it up to not catch on quoted or binding forms of the symbol. But yeah, add in all that complexity and you might as well reach for the analyzer

john16:02:58

I was implementing in cljs though

bronsa16:02:07

well in cljs you have access to cljs.analyzer :)

john16:02:28

The same should be doable with that, yeah?

john16:02:39

Doesn't have as many bells and whistles as your c.t.a.jvm I thought

bronsa16:02:13

it doesn't indeed but the AST format now matches t.a so you should be able to use the passes and the rest of the framework over the AST it produces

bronsa16:02:32

I haven't tried it but I think @U055XFK8V does that

john16:02:50

Wait, you can use c.t.a.jvm on cljs forms?

bronsa16:02:33

you can use cljs.analyzer, get back an AST and feed that AST to tools.analyzer passes/traverse it using the t.a utilities

john16:02:10

Well that's awesome

bronsa16:02:18

don't know how easy or feasable that is, but some work went into cljs last year to allow this

john16:02:00

Ain't nothin a little elbow grease can't fix

john16:02:25

Big appreciation to you and bronsa's work on it

bronsa16:02:00

that would be the same person simple_smile

john17:02:06

lol ambrose I mean! 😂

john17:02:19

bronsa, ambrose, tomato, tomato

john16:02:19

You just call expr-has? in your macro. You may need to check for both the namespace qualified and non-namespace qualified version of the symbol. But that's extra macro stuff.

bronsa16:02:21

(require '[clojure.tools.analyzer.jvm :as j])
(require '[clojure.tools.analyzer.ast :as ast])

(defn find-undefined-locals [expr]
 (->> (j/analyze expr
                 (j/empty-env)
                 {:passes-opts (merge j/default-passes-opts
                                      {:validate/unresolvable-symbol-handler
                                       (fn [_ s {:keys [op] :as ast}]
                                         (if (= op :maybe-class)
                                           ast
                                           (throw (Exception. ""))))})})
      ast/nodes
      (filter (fn [{:keys [op]}] (= op :maybe-class)))
      (map :class)))

(find-undefined-locals '(let [a identity] (a b))) ;=> (b)
@dpsutton @nbtheduke

bronsa16:02:59

in your example if you use (find-undefined-locals expr) you should be able to find the set of locals that you need

bronsa16:02:45

it's quite the hammer though :)

bronsa16:02:15

if I had a timemachine I would rename that node type to :undefined-symbol but alas

Noah Bogart16:02:21

With something like this, is it happening at compile time or run-time? heh cuz I suspect the overall performance cost of doing this at runtime will overshadow the performance gain from skipping the unused variables

bronsa16:02:19

you're invoking it from a macro

bronsa16:02:21

so compile time

bronsa16:02:05

this is assuming that you do something like (defmacro whatever [body] (let [neded-locals (find-undefined-locals body)] (emit-only needed-locals body)))

bronsa16:02:18

which afaict is the only way to do what you want anyway

Noah Bogart16:02:07

Where is emit-only coming from?

dpsutton16:02:16

that's what you write to create only the binding forms that are in the set needed-locals

dpsutton16:02:40

it's the binding forms of your let form in the original macro, moved to a function to make it easier to test and the macro easier to reason about

Noah Bogart18:02:32

Having messed around with this for a bit, I have a small question: how do you go about let binding the needed-locals? I can construct a vector of the necessary forms, but I'm having trouble using that vector in a let as the bindings. Or is there some much easier solution to this?

dpsutton18:02:03

if you know the necessary forms its simple (let [~@ forms] body)

dpsutton18:02:20

macro land you can just plop the pairs in there

Noah Bogart18:02:57

Ah, okay, thank you, that was the part I was missing

Noah Bogart16:02:35

Ah okay, thanks, that's cool

Noah Bogart16:02:44

I'll mess around and hopefully make it work!

bronsa17:02:12

just added a slightly more performant version of find-undefined-locals to https://github.com/Bronsa/tools.analyzer.jvm.deps/commit/8c7c3936e6f73e85f9e7cc122a2142c43d459c12

dpsutton17:02:03

if you're gonna bump some stuff, the project.clj version is out of date with the pom. little confusing

dpsutton17:02:17

project.clj is 0.6.9 snapshot or something? and released is 0.7.0

bronsa17:02:26

this is t.a.jvm.deps

bronsa17:02:34

but I can fix that in t.a.jvm

dpsutton17:02:43

i think that might just be t.analyzer

dpsutton17:02:52

i had to read t.a.jvm to see what version was actually out

bronsa17:02:19

the stuff in the readme should be correct

bronsa17:02:31

it's just project.clj that sometimes gets out of sync because I forget, I think

bronsa17:02:38

I'll double check both and update though, thanks

🎉 5
Twice17:02:38

I've never needed to have a caching in my Clojure program before, so I need some guidance. I have a collection of entities (maps), that are calculated via some expensive functions using data-fetching from the database. Basically, I want to have it in cache as a big map with entity-id as keys and corresponding hash-map as values. I looked up idiomatic ways to handle caching in Clojure, it's suggested to use clojure.core.cache and atoms. It seems to me that none of eviction policies are suitable for my case, so I resorted to use BasicCache. Additional to my "main" cache, implemented as an atom which contains plain entries, I want to have a nested lookup map with same entities grouped in keys for fast access. I want to store this derived lookup map in some atom for future uses, rather than calculating it every time (it's ~ over a million values). I have two questions: 1) Should I use clojure.core.cache's BasicCache? It seems to me that it's just a wrapper over regular map, what benefits do I get using it? 2) Is it ok or idiomatic to use add-watch as a way to keep in sync two atoms? So that my derived-lookup-map in second atom is always relevant and in sync whenever plain data in first atom changes?

isak18:02:46

@alisher.atantayev Not sure about BasicCache, but for 2, on the client a lot of people would just use re-frame subscriptions for that, which lets you achieve the same with more precision and less imperative code. Not sure if anyone uses that on the server, though

Twice04:02:56

Yeah, I was looking for something like signals/subscriptions, but on the server-side. Was thinking watches would provide same functionality.

noisesmith20:02:34

Keeping atoms in sync is a domain error - it's not what atoms are for. You can synchronize changes to refs to ensure that they stay coherent, but it's not something you can do generally / safely with atoms.

noisesmith20:02:26

the advantage of BasicCache is that it uses the cache protocols, so you can swap in other caches with other behaviors without changing surrounding code

noisesmith20:02:13

eg. you thought you needed a TTL cache, but now product says no more time outs, BasicCache is a simple change without rewriting surrounding code

noisesmith20:02:43

or, you know you want a cache, but specific behaviors are going to be elaborated as needed, so you start with a BasicCache so you don't need to rewrite code to get the new caching behaviors in the final result

noisesmith20:02:29

if a second atom is always tied to the state of a first atom, and isn't changed itself, you can replace it with a function on the first atom, or added fields in a map in one atom

kenny21:02:20

Is it possible to add a Java annotation to a Clojure function?

noisesmith22:02:10

I'd expect you'd want an annotation on a class, not a function per-se?

kenny22:02:30

Well, a method would be more correct I suppose.

kenny22:02:25

I'm trying to do this:

@Trace (dispatcher=true)
public void run() {
  // background task
}
without needing to do some crazy definterface & deftype stuff.

noisesmith22:02:45

well, you need a class with a run method that's annotated, you can use gen-class and get a method without an interface, but the idiomatic thing in clojure is to use defprotocol (which defines an interface) then defrecord, deftype, or reify to get something implementing it

noisesmith22:02:15

unless the method name doesn't actually matter, in that case functions have an invoke method, but there's no way to annotate it

noisesmith22:02:41

you could reify or otherwise implement clojure.lang.IFn, and that way get something that calls like a normal function and also has annotations

kenny22:02:09

Hmm that's interesting.

kenny22:02:27

The idea is to be able to add that annotation to any Clojure function.

kenny22:02:33

Is IFn the only one I'd need to implement?

noisesmith22:02:04

yeah, one moment, I have an example somewhere

kenny22:02:34

Can you dynamically set annotations on a deftype?

kenny22:02:02

Something like:

(deftype TracedFn [traced-params]
  IFn
  ((with-meta 'invoke traced-params) [_]))

kenny22:02:23

I don't think the deftype params are in scope. Even this doesn't work:

(deftype TracedFn [traced-params]
  IFn
  (^{com.newrelic.api.agent.Trace traced-params}
  invoke [_]))

kenny22:02:12

I guess I can macro-ify that.

noisesmith22:02:05

when you say it doesn't work, you mean because you can't have a different set of annotations for each instance?

kenny22:02:25

Sorry, I should've been more specific. I get this:

Syntax error compiling at (impl.clj:19:1).
Unable to resolve symbol: traced-params in this context

noisesmith22:02:30

even with java. I don't think you can have different annotations on methods each instance of a class

noisesmith22:02:41

oh, yeah, it prevents capture in deftype

noisesmith22:02:03

I'd just use reify unless you really need multiple instances (why? I don't make multiple instances of functions...)

noisesmith22:02:24

otherwise, yes, a macro that fills it in, since lexical capture is unavailable in deftype / defrecord

kenny22:02:05

Right, makes sense. Thinking the IFn path is the easiest. No need for an extra interface here.

noisesmith22:02:25

be sure to define every applicable arity for invoke, plus applyTo

noisesmith22:02:33

:thumbsup: (bad emoji autocomplete haha)

kenny22:02:15

It seems to work without defining every arity. Is that just a REPL artifact?

noisesmith22:02:35

if you don't define applyTo, apply breaks

noisesmith22:02:00

I'd rather take the time to define applyTo, rather than risk a really counterintuitive error if I ever refactor and use apply

noisesmith22:02:15

you only need to define the arities you implement, of course

kenny22:02:18

So I could just define the invoke method(s) I need and applyTo?

noisesmith22:02:08

if you need numeric perf, the method overloads for primitives are there too...

noisesmith22:02:35

(where fn gives you that "for free" if you satisfy the static constraints the compiler knows how to use)

kenny22:02:39

I was wondering what all those were. Looks like generated code at first glance.

noisesmith22:02:07

pretty much, yeah, the multiple overloads is how you get good fast paths (at the expense of human radability)

kenny22:02:41

Is the typical way applyTo implemented with a case statement of the count of the args?

noisesmith22:02:45

nice to see the kind of bug I was avoiding by just superstitiosly using applyTo even though I didn't think I needed it :D

😄 5
kenny22:02:27

FWIW, ended with :

(defn- invoke-methods
  [bodies meta-map]
  (for [[args-list & body] bodies]
    `(~(with-meta 'invoke meta-map) [~'_ ~@args-list] ~@body)))

(defn- make-traced2
  [deftype-name traced-meta fn-name bodies]
  `(deftype ~deftype-name []
     IFn
     ~@(invoke-methods bodies (default-traced-meta traced-meta fn-name))
     (~'applyTo [this# args#]
       (AFn/applyToHelper this# args#))))

noisesmith22:02:12

with a bit of wrangling you could turn that into fn-traced or traced-fn or whatever

kenny22:02:27

That's the end goal 🙂

kenny22:02:46

I wish Clojure had some helper functions for parsing defn args... going to attempt to do that with spec

noisesmith22:02:01

so you can't just delegate by passing in the argslist as is? or is it that deftype methods don't support destructuring etc?

kenny22:02:07

It's just the complexity of the defn spec

kenny22:02:39

Gotta take in all this stuff

'([name doc-string? attr-map? [params*] prepost-map? body]
                [name doc-string? attr-map? ([params*] prepost-map? body) + attr-map?])
and spit out a defn matching what I was given.

noisesmith22:02:41

ahh, if you want 100% compatibility I can see that...

noisesmith22:02:04

I'd be tempted to just support what I consider "normal" in defn

kenny22:02:46

Yeah that wouldn't be the end of the work, I suppose.

kenny22:02:05

I'm kinda surprised no one has a library for writing defn-esque things.

noisesmith22:02:50

yeah - they made one for map-like things...

kenny21:02:10

I've seen this: https://clojure.org/reference/datatypes#_java_annotation_support which sounds like the answer is no.

Lennart Buit21:02:42

it looks pretty magical tho

Evan Bowling23:02:36

What do people think is the oldest clojure version a library should support?

andy.fingerhut23:02:27

For a brand new library, Clojure 1.9.0 would seem reasonable.

andy.fingerhut23:02:54

The latest Clojure survey results show that 97% of respondents were using Clojure 1.8.0 and later: https://www.surveymonkey.com/results/SM-S9JVNXNQV/

Alex Miller (Clojure team)23:02:59

Well that was a multi select question so careful with the number