Fork me on GitHub
#clojure
<
2023-01-17
>
apbleonard10:01:13

Silly question I'm sure - but has anyone good naming convention for a symbol representing a sequence results that may be short in tests but very long in production, e.g. drawing from a large database query cursor, that must be consumed efficiently to avoid memory problems? I find folks can't resist sticking a (count results) somewhere or otherwise holding on to the head. I hoped results... would be legal but it's not. Perhaps results-> or even results! to show how you consume this sequence may have side effects?

practicalli-johnny10:01:27

Seems more like a systematic challenge rather than a syntax one. Not sure how adding some kind of character will stop behaviour without discussing the underlying issue Maybe just talk to the team and see what would help them

p-himik10:01:48

I wouldn't resolve to some "easy" convention in such a case. ... is ambiguous (even if it was working), -> and ! are overloaded. If you really want to try solving it with naming, I'd use results-LL ("lazy and large") or results-On (for O(n)). Or even go verbose and use large-and-lazy-results. A more robust solution IMO would be to wrap that collection and in all relevant methods check if some dynamic variable is set, then set that var in a macro so it ends up being something like

(with-awareness-of-large-results
  (count results))
Another approach is an architectural one - never let results float outside of some specific high-level functions that accept whatever function you might need to apply to every result or to the collection as a whole.

apbleonard11:01:41

Thanks - interesting approaches. @U05254DQM - agree that this is a team challenge and we should just talk about it. @U2FRKM4TW - verbose naming probably the pragmatic way to go but your robust solutions are interesting too. Perhaps use of sequences is simply so idiomatic to Clojure that folks really need to learn that style, rather than hand-holding them too much by adding macros etc... That said, closing over the cursor and only allowing you to pass a function to map over might work for me ....

apbleonard11:01:37

Hmm ... except you could pass it identity and get back the cursor again.... Perhaps close over with a function that applies dorun to any (side-effectful) function passed in? Seems yuck but ...

p-himik11:01:39

> except you could pass it identity and get back the cursor again That solution is, in my mind, not intended to prevent anything. It's intended to guard in a much more explicit way than just naming and to add a potential barrier in front of doing something bad.

👍 2
dpsutton11:01:01

Not sure if it fits your exact pattern but using reducible queries helps makes this distinction. The transducer context can be quite nice to kinda keep things local and iterate once

💡 2
didibus16:01:10

I use the ! pattern to denote "be careful", meaning go look at the doc for it. If it was a local it be a comment instead of a doc-string. And then the comment/doc-string would say that results can contain a very large N, something like that.

didibus16:01:37

But not everyone uses ! to mean that.

didibus16:01:34

You could also go real verbose if you're really worried a out it: resutls-do-not-hold-on-to-head or results-is-very-large-coll-be-careful

didibus16:01:51

Also, just a comment might be enough

phill16:01:57

You can require a LIMIT clause on the sql cursor statement. If someone writes code that specifies LIMIT 3 then it's ok to count the results. But if they say LIMIT 1000000000000000000000000000000000000000000000000000000000000000000 then they should know to tread carefully.

Algo_20213:01:56

I have a file email.clj as follows:

(ns product.email
(:import [com.sendgrid.helpers.mail.objects Personalization])
(:require [taoensso.timbre :as timbre])
(:gen-class))

(defn log-email
  [message]
  (timbre/report "\nTo: (.getTos message)))
On compiling the project, it is giving an error: java.lang.IllegalArgumentException: No matching field found: getTos for class com.sendgrid.helpers.mail.Mail I have two questions: 1. Why the error message is pointing to the class com.sendgrid.helpers.mail.Mail when I have only imported [com.sendgrid.helpers.mail.objects Personalization] in the file above? 2. getTos is defined in [com.sendgrid.helpers.mail.objects Personalization]. Why is the compiler not recognizing it?

dpsutton13:01:45

What are you calling at the REPL?

dpsutton13:01:38

And perhaps something that could illuminate, what do you think should happen if you called (log-email []) from the repl?

dpsutton13:01:47

❯ clj
Clojure 1.11.1
user=> (.getTos [])
Execution error (IllegalArgumentException) at user/eval1 (REPL:1).
No matching field found: getTos for class clojure.lang.PersistentVector
user=>
So recreating the experience here, Clojure is trying to call the method .getTos on a vector. That method doesn’t exist on the object i called it on so the message is a rather sensible > No matching field found: getTos for class clojure.lang.PersistentVector

Algo_20213:01:09

@U11BV7MTK edited the question. I am running the project with lein repl and a frontend action is calling the function

dpsutton13:01:01

that’s what i figured. Does my example give any help?

msolli13:01:38

You’re calling log-email with an instance of the class com.sendgrid.helpers.mail.Mail. What you’re importing in the ns doesn’t really matter here.

2
skylize14:01:38

You say that "`getTos` is defined in ...`Personalization`" . Perhaps that is where the current problem is, and has nothing to do with this function at all? Somewhere, in your project, a piece of code that is getting evaluated tries to call .getTos method of a com.sengrid.helpers.mail.Mail, which doesn't have such method. That would not be happening in the log-email function unless you are actually calling log-email. The code shown so far never actually calls that function, so you are not showing us what inappropriate value is being passed into the function.

mkvlr15:01:38

hey 👋 anybody know why clojure.tools.analyzer.jvm/analyze would redefine the defrecord and how to avoid it? Gist with repro in 🧵

borkdude15:01:14

I guess it's related to this hack:

;; HACK
(defn -deftype [name class-name args interfaces]

  (doseq [arg [class-name name]]
    (memo-clear! members* [arg])
    (memo-clear! members* [(str arg)]))

  (let [interfaces (mapv #(symbol (.getName ^Class %)) interfaces)]
    (eval (list `let []
                (list 'deftype* name class-name args :implements interfaces)
                (list `import class-name)))))

Ben Sless15:01:16

iirc it's a hack relating to how analysis works

borkdude15:01:50

I guess the hack could be avoided when the type already exists and matches the code that's being analyzed?

borkdude16:01:00

@U5H74UNSF This is a hack which avoids the hack:

(do
  (let [form '(defrecord MyRecord [form])
        old-deftype-hack ana-jvm/-deftype]
    (with-redefs [ana-jvm/-deftype (fn [name class-name args interfaces]
                                     (when-not (resolve class-name)
                                       (old-deftype-hack name class-name args interfaces)))]
      (ana-jvm/analyze form (ana-jvm/empty-env) {}))) :analyzed)

clojure-spin 2
borkdude16:01:46

@U060FKQPN There is no clean way to customize the deftype* analysis to avoid that eval side effect right?

bronsa16:01:51

not really, FWIW it's something that the clojure compiler also does internally, it's related to having the ability to correctly do type analysis on deftype methods

Ben Sless16:01:19

You could check if the class already exists, no?

bronsa16:01:39

well, ish

😅 2
bronsa16:01:53

it must exists and match the definition under analysis

bronsa16:01:27

if anybody wants to make a ticket I could spare some time to have a look at it, unless somebody also wants to provide a patch :)

Ben Sless16:01:28

Yeah Which you can't really verify in a repl

Ben Sless16:01:03

How? You could check the signatures match, but the implementation?

borkdude16:01:16

does the impl matter for analysis?

bronsa16:01:28

yeah sorry, I said implementation but I mean interface

bronsa16:01:46

wait, I said neither 😄

bronsa16:01:58

but yeah, the impl doesn't matter. just the signatures of the methods it implements

Ben Sless16:01:33

So it should be possible for deftype, but not for reify, right?

borkdude16:01:48

we could also have an option to "assume that a same-named class will have been evaluated with the same form before" or so

bronsa16:01:14

no real difference between deftype and reify at that level

borkdude16:01:44

as to not overwrite existing impls which isn't great for working in the same environment

bronsa16:01:15

another approach would be to prefix the class used for type analysis, but then we need to be careful to rename things correctly everywhere

bronsa16:01:25

... yet another approach would be to define this class in an isolated classloader

bronsa16:01:19

FYI I have a comment here that may be a bit annoying to hear, but: this in general should not be a problem that exists, because t.a. fundamentally works with analyze+eval, not just analyze

bronsa16:01:46

but ok, there are options we can consider to make this a bit nicer

bronsa16:01:01

just not one that's currently implemented I'm afraid

bronsa16:01:55

I think you could do the analysis binding clojure's classloader to an isolated one

bronsa16:01:11

and that may make the redefined class not leak into your classloader

bronsa16:01:20

I can have a go at an example in a few hours, a bit busy now

borkdude16:01:39

:bindings {Compiler/LOADER ...} yeah, just gotta figure out how to do this

👍 2
bronsa17:01:07

makeClassLoader creates a new DynamicClassLoader based off of Compiler/LOADER

😞 2
bronsa17:01:18

the classCache will still be a problem though

bronsa17:01:32

so yeah, not straightforward

bronsa17:01:09

there are hacks around it

borkdude17:01:26

I wonder why this doesn't crash:

(with-bindings {clojure.lang.Compiler/LOADER (proxy [ClassLoader] []
                                                 )}
    (eval '(deftype Dude1 [])))

bronsa17:01:32

user=> (defmacro hack-loader [] (var-set clojure.lang.Compiler/LOADER nil))
#'user/hack-loader
user=> (eval '(let* [] (hack-loader)))
Syntax error (NullPointerException) compiling fn* at (REPL:1:8).
Cannot invoke "clojure.lang.DynamicClassLoader.defineClass(String, byte[], Object)" because "this.loader" is null

bronsa17:01:49

here's a very hacky way to set the loader

hiredman17:01:03

potemkin has some custom deftype/defrecord like macros that attempt to avoid re-defining classes if they already exist, and people also run into problems with that behavior

mkvlr19:01:07

@U060FKQPN couldn’t quite figure out yet how to pass an isolated classloader to tools.analyzer yet. Do you have more pointers for me? 🙏

mkvlr20:01:24

also wondering if you could expand a bit on your comment regarding tools.analyzer fundamentally working with analyze+eval. I understand that any macros need to be evaluated before so analysis can use them, same for fns used by macros. Beginning to think the same is true for protocols / deftypes.

bronsa23:01:13

I don't have a good working suggestion for you yet, give me a few days, I'll try to get you something working on the weekend

mkvlr18:01:52

thanks a ton for your help!

bronsa23:01:13

@U5H74UNSF currently the only suggestion I have for something that can work for you would be to with-redefs eval to be a no-op during analyze. (i.e. (with-redefs [eval (fn [x] nil)] (ana.jvm/analyze ..))) I am exploring options but it will require some time

👍 2
bronsa23:01:18

as per my comment wrt analyze+eval -- yes, your idea about marcors needing to be evaluated + all their dependency tree is correct, and that's the main point. and for deftypes we also need to do this to proved correct method resolution. it is also however true that if 100% analysis accuracy is not required, t.a does expose some hooks where the user can plug in and provide escape hatches to avoid needing full evaluation, and this one seems like it could be a good candidate for one

mkvlr15:01:28

makes sense, thanks for clarifying