Fork me on GitHub
#clojure
<
2024-02-13
>
danielneal11:02:16

Dumb question about the new method values - will (map String/toUpperCase ["hi" "there"]) work, and use reflection, but be slower, and (map ^[] String/toUpperCase ["hi" "there"]) recommended for speed, or will only the second option work.

p-himik11:02:41

It was fewer characters to test it than to write the question. ;)

$ clj -Sdeps '{:deps {org.clojure/clojure {:mvn/version "1.12.0-alpha7"}}}'
Clojure 1.12.0-alpha7
user=> (map String/toUpperCase ["hi" "there"])
Syntax error (IllegalArgumentException) compiling at (REPL:1:1).
Multiple matches for method toUpperCase in class java.lang.String, use param-tags to specify

danielneal11:02:20

Haha maybe for you

p-himik11:02:43

But now you know how to do it too!

danielneal11:02:19

I did try with cider, to be fair, but was getting an error in my scratchpad environment - I think 1.12.0-alpha6 had an issue somewhere

borkdude13:02:30

yes, upgrade to alpha7, CIDER doesn't work with alpha6

❤️ 1
Noah Bogart14:02:42

Clojure 1.12 feedback (https://ask.clojure.org/index.php/13710/method-values-and-syntax-errors): Someone on reddit brought up the point that by treating ambiguous method values as syntax errors, the code is now more brittle than using the old interop syntax because in Java, adding an overload (by arity or by type) is a non-breaking change. This can lead to forcing one to use a specific version of a Java library merely to continue using a Clojure library that uses the new method values syntax, because later versions of the Java library provide overloads that make the Clojure code ambiguous where it previously was not. Given Clojure's historically lenient and "trust the programmer" stance on interop (`(.foo {})` compiles just fine even tho {} is a literal of known type), it seems odd that this new syntax throws syntax errors instead of falling back to reflection (with an associated reflection warning).

jjttjj14:02:51

Just to make sure I get it, you're saying you prefer that one be forced to do:

(map ^[] String/length ["abc" "abcd"])
And not allow:
(map String/length ["abc" "abcd"])
because the latter would break if another method overload was added to String/length?

Alex Miller (Clojure team)14:02:34

Yes, this is a new syntax that makes different choices. With param-tags, you can choose the specificity you want depending on whether you think this is important. Because param-tags specify arity, adding new arities is NOT a breaking change. Adding a new overload in a parameter that was previously specified by a wildcard would be, and you can avoid that by being more specific in your param-tags.

Noah Bogart14:02:36

I personally am wondering about the decision to not fall back to reflection when given ambiguous method values, given Clojure's existing allowance and reliance on it. Feels sharply incongruous

1
Alex Miller (Clojure team)14:02:47

many people doing production Clojure work very hard to avoid reflection and will find the guarantee of no inference to be a welcome option

3
Noah Bogart14:02:04

The original poster is worried about people using no param-tags or wildcard param tags when it's unambiguous and then the underlying java library adding overloads which make it ambiguous and causing syntax errors when previously there were none

Alex Miller (Clojure team)14:02:03

well, I think there are many people that welcome an error over reflection in this case

4
valerauko14:02:27

> a welcome option is it an option? eg can you opt out of this new syntax check?

Noah Bogart14:02:53

Sure, I understand the desire to not have any inference, I also work hard to avoid it. I don't know that I see the value in a hard error vs emitting a reflection warning, when it can affect downstream users

Noah Bogart14:02:16

You can opt out in your own code by not using it. You cannot opt out in libraries that use it.

👍 1
2
jpmonettas14:02:56

also, if the situation arises in a library, now you don't have a error but a new reflection warning you "can't fix", but at least it is not an error

Alex Miller (Clojure team)14:02:03

but if you require no reflection it IS an error

Alex Miller (Clojure team)14:02:58

if a library uses param-tags and specifies types, then they will never encounter this situation, so maybe that's a best practice for lib authors

jpmonettas14:02:23

but I think it is very easy to forget, because when you are developing your lib, if there is only one option, you just call the thing with the shortest syntax

Noah Bogart14:02:09

Maybe removing wildcard/requiring a param-tag would be helpful here. Enforce that it has to be explicit, cannot be ambiguous under any circumstances. That would solve the issue of a java library adding an overload and breaking the clojure code

1
Alex Miller (Clojure team)14:02:50

seems like a good job for a linter. I don't think it makes sense for clojure to require that

jpmonettas15:02:17

but if the trade offs are possibly breaking vs possibly reflecting, isn't the second one less harmful?

Alex Miller (Clojure team)15:02:41

for many people, no, I don't think so

jpmonettas15:02:25

imho people interested in avoiding reflections are already using the warn-on-relection flag, so they are going to see it also

2
Alex Miller (Clojure team)15:02:37

that's a different level of "see"

Alex Miller (Clojure team)15:02:44

taking something fast and making it slow is a breaking change in many scenarios

danielneal15:02:51

you mean from accidental reliance on timing?

ghadi15:02:58

as a Datomic dev, I'm one of those people. We do not want accidental reflection

Alex Miller (Clojure team)15:02:03

no I mean from production requirements

jpmonettas15:02:21

I agree, but it is a scenario that will not be invisible for the reflection interested people (I consider myself one of those)

ghadi15:02:50

we compile against a specific JDK release, and will never be broken by new overloads because the method selection is only a compile time thing

ghadi15:02:57

we don't load from source

ghadi15:02:04

^[long] Thread/sleep added an overload between 17 and 21

ghadi15:02:22

we are not broken by that because we already compiled and selected the method

jpmonettas15:02:04

but I think this was more about upgrading java libraries

oyakushev15:02:27

Sounds to me like worrying over a thing that hasn't happened yet and wouldn't happen too often. With clear ways to prevent it (spelling out full signature instead of wildcards).

Noah Bogart15:02:19

I think it's right to worry about things that haven't happened but could during alphas. Now's the time to change them if they would be changed

4
oyakushev15:02:39

I would challenge the premise that third-party Clojure libraries are exemplary at being future-proof. Stuff occasionally breaks on upgrades, for various reasons, it's not a once-in-a-lifetime event. I'm not sure this hypothetical issue makes the situation much worse in terms of overall reliability.

jpmonettas16:02:28

as I see it, the argument is not about making libraries future-proof, more about this decision of throwing instead of reflecting in this particular case, while I think Clojure so far doesn't break if it can reflect, and then we have the warn-on-reflection tool for when we are interested in perf.

2
👍 1
Noah Bogart16:02:00

after reading the convo, i think the original reddit post's position is overstated. It's something to be careful of but not nearly as dire as they say. (only applies if no param-tag or wildcard param-tag) i personally would prefer it to issue a reflection warning instead of a syntax error but it is what it is

2
oyakushev16:02:23

It could be so that wildcard hint would become a bad practice to use in libraries (same as leaving out significant type hints now). It is bad either way, reflection or compile-time error.

jpmonettas16:02:51

I agree, practically it will not happen very frequently, and when it does happen, even in libraries, there are escape hatches since most libraries are open source, we can always fork, etc. It just feels weird, since for us interested in avoiding reflections, we will still have to look at all reflections warnings anyway, so that throwing will not help a bit. Also I guess this alphas are the time for bringing all this questions.

2
seancorfield17:02:17

I haven't read the r/Clojure post (yet) but my question would be "Who is updating the Java library/JDK version?" -- It can't be the library author because they'd see the breakage and fix it so it must be the user of the library and, yes, if you update a Java library that one of your dependencies relies on and it changes anything, you might break the library. It's pretty common in the Java world for even a patch release update to break downstream code. So I think the concern is way overblown. Now, that said, we are very aggressive about updating dependencies at work so we are "likely" to run into this theoretical... and I definitely want code to "break" (fail to compile) rather than silently switch to a slower implementation (or, worse, accidentally call a different overload that now happens to match better than the old code).

seancorfield17:02:04

(we have a reflection check as part of CI and fail the build if either new reflection warnings appear or a new file doesn't have (set! *warn-on-reflection* true) in it)

seancorfield17:02:06

Oh, and it turns out I have read that post and already replied yesterday that I'd prefer an error to silent reflection 🙂

😅 1
seancorfield18:02:54

I've responded similarly to the Ask post -- because I think it's really important to question why you'd be updating a Java library that one of your dependencies uses. That's often going to be a breaking change in some way or other already...

👍 1
jpmonettas18:02:48

it doesn't need to be you explicitly updating a java library, it could be a transitive dependency

jpmonettas18:02:05

so, it is possible that if you depend on A and B, you upgrade A and then B breaks if they both depend on the same Java lib but A has moved to a newer version

seancorfield18:02:39

As I said on Reddit, if you update a dependency and Java libraries are involved somewhere, breakage is already fairly common, often in subtle ways...

jpmonettas18:02:18

but shouldn't because of overloading

seancorfield18:02:27

This is already a problem with large Clojure codebases that have a large number of (transitive) Java dependencies.

danielneal18:02:51

Would it be wrong to say that this concern is only about future regressions once this syntax is in the wild? At the moment all of this sort of interop is reflective as the method is not selected at all? It also seems like the changes we are discussing are accretive changes to java libraries. They are providing more by adding overloads and in those situations code might either break with a syntax error, or break by slowing down / using reflection. But we (in the cases being targeted) would have been using reflection anyway prior to 1.12 so is reverting to using reflection so bad?

☝️ 1
1
seancorfield18:02:53

The change to Thread/sleep was a breaking change for us -- because CI fails the build if new reflection warnings appear.

seancorfield18:02:11

"We" (at work) would not have been using reflection so, yes, removing the "no reflection" guarantee from the new syntax is bad as far as we are concerned.

danielneal18:02:12

Is what you are saying @U04V70XH6 is that because the reflection warning is only in CI you'd catch the error earlier as a syntax error? And that's preferable?

ghadi18:02:57

let's keep in mind the scenario at play here: you load code that refers to a java method by using param-tags with a wildcard you load the same code against a different version of the Java library where the given param tags (with wildcard) is no longer perfectly selective. Compiler rejects ambiguity.

danielneal18:02:49

*is omitting the param tags in unambiguous cases also a wildcard?

Alex Miller (Clojure team)18:02:05

it's not a wild card but it presumes one arity

ghadi18:02:56

There is no change to anything that exists today*. :param-tags are new railroad tracks, no changes to existing tracks. (*) there is a subtlety around static methods in invoke position

Alex Miller (Clojure team)18:02:05

I think the time we've spent in this thread is about the same amount of time it will take to resolve all the cases that happen in the next 5 yrs or so

😂 8
Alex Miller (Clojure team)18:02:57

the jdk has had changes like this that affected us I think 3 times in the last decade

danielneal18:02:19

I definitely trust a your process and appreciate the care the team put in and sounds like you've thought about this and are confident about the change

Alex Miller (Clojure team)18:02:50

I understand the concerns, I appreciate them being raised, I don't think the magnitude of problem is proportional to the concern expressed, but time will tell.

Alex Miller (Clojure team)18:02:15

there is nothing preventing us from further refinement in the future

jpmonettas19:02:01

again, imho the original discussion wasn't about this situations being hard to fix (could be if the lib in question is closed source, which is rare) but about why the decision of not falling back to reflection when all interop so far does

👍 1
ghadi19:02:44

(interop doesn't fall back to reflection. The compiler makes a one-time decision when compiling: it found exactly one method or it didn't. Once code is compiled, that decision is never revisited.)

Alex Miller (Clojure team)19:02:36

inference and reflection played a prominent role in the early days of Clojure because there were other Lisp/Scheme/Clojure variants and a premium value on concision in that space at the time. in the intervening years, people have built a lot of commercial products and services, and something we see frequently are requests for ways to prevent and avoid reflection (this has mostly gone into linters and tools). being more specific means being more verbose, but the benefit is performance and reduced ambiguity. these are points on a spectrum. how will it all play out in the ecosystem? hard to say without more experience.

👍 3
1
jpmonettas19:02:43

> (interop doesn't fall back to reflection. The compiler makes a one-time decision I know, but only for this case, in that one time decision, it doesn't consider reflection

vemv19:02:32

My 2c, the incongruous remark is fair, but introducing new syntax seems a good opportunity to make said syntax as strict as possible without breaking anything else (as it's new) Getting an error at compile-time is practically the best level of feedback one can get - it's immediate and official. Contrariwise, offloading errors to a linter doesn't always work (clj-kondo doesn't currently warn for specific reflection warnings, and if Eastwood does it's only because it wraps Clojure's compiler - was a lucky strike)

👍 1
1
1
Noah Bogart19:02:14

I'm reminded of https://ask.clojure.org/index.php/13339/tools-better-detect-errors-coming-existing-methods-fields that was about this very thing, lol. It seems the Uniform Class/member syntax has provided a solution to what both @U45T93RA6 and I were requesting

🙂 1
jpmonettas19:02:47

@U064X3EF3 I'm under the impression that the advice was don't type hint and leave it to reflection unless you have measure and that is causing a perf problem. Not because of verbosity but because of flexibility. Out of curiosity, has yours or others opinion on this changed from experience? I see this being interesting from a library perspective where we don't know the context our code will be running in.

Alex Miller (Clojure team)19:02:03

many production users of Clojure just endeavor to remove all use of reflection

Alex Miller (Clojure team)19:02:17

if you have millions of lines of Clojure code, case by case is harder than blanket policy, the scale matters

👍 3
1
seancorfield19:02:43

(we're at 140K+ lines -- not "millions" yet but large enough that this sort of thing matters to us)

oyakushev19:02:08

I see reflection as a convenient devtime tool, it's good not having to hint immediately as you write out a function. But there's little reason to keep reflective invocations after you're done.

1
imre13:02:15

Lots of good points in the thread, thank you everyone. I share the worry that being too liberal with the new syntax might result in brittle 3rd party libs. (similarly to how some other constructs like https://ask.clojure.org/index.php/10383/using-proxy-might-make-otherwise-additive-change-breaking are brittle right now - although there's little the dev needing to use proxy can do here) If I may add my 2 cents, my preferences would be: • do not fall back to reflection • always require param tags • do not allow wildcards in param tags

Noah Bogart14:02:29

I had the same thought, @U08BJGV6E, so i added a new rule to my linter https://github.com/NoahTheDuke/splint to help enforce that.

1
phill16:02:34

Clojure has been known for graceful longevity of programs. Write once, run forever. Clojure is known for what R.Hickey called "situated software", which motivated JVM hosting but also embraces orgs with an IT department that might demand certain updates at certain times. Those new arities will appear at times not chosen by the program's attendants. Compile-time in Clojure may be every time the program runs. For every 100k-line codebase, attended by a staff that can attend to reflection warnings, there may be thousands of little programs that will simply stop working. If Clojure is to offer a concise, easy syntax and a tedious more-precise syntax, then the easy syntax should be forgiving (via reflection) to preserve Clojure's good reputation for graceful longevity. If someone cares enough about avoiding reflection to use the tedious syntax, then they can do that.

2
oyakushev21:02:28

They will not suddenly stop working without bumping at least some of the dependencies. The level of dramatic exaggeration in this thread is mind-blowing:). Java libraries (and Clojure libraries too) often introduce breaking changes, so you will have to check and fix things when you upgrade dependencies anyway.

1
☝️ 1
imre21:02:34

My general worry is not about code I/my team need to update should some transitive dependency get bumped. It's that one clojure lib in the middle that no longer receives updates, because, well, it's done. And yes I know there are such breakages currently, it's just that there's a chance now to not introduce another possible breaking point. Or at least make it somewhat less likely to suffer a breakage, by enforcing a bit of discipline.

phill23:02:09

> Java libraries ... often introduce breaking changes, At issue are changes that are not even regarded as breaking. A new arity or overload is purely additive in the explicitly-typed realm. Elsewhere in Clojure, the easy syntax is resilient & possibly slow & can display a warning when reverting to reflection. That would be nice here too. The high voltage, "death before reflection" 🙂 contingent is able to look out for itself.

Alex Miller (Clojure team)23:02:24

Java libs introduce breaking changes all the time (more frequently than they introduce overloads which are already sometimes breaking). The fear in this thread is not proportional to the effective change. Novelty happens in the world, when it breaks stuff, we fix it - that is true both before and after. This new syntax is not a radical change in this area. If this is judged to be a problem after months or years of experience, we have many options open to address.

👍 1
phill00:02:32

Case in point is DXML-66, a still-open reflection issue in clojure.data.xml caused by *Java 10'*s addition of an overload to http://java.net.URLDecoder. Also Pedestal issue 689, for the same issue: closed more than a year after being reported. Stepping back to reflection has afforded data.xml and Pedestal maintainers a lot of grace to deal with these issues. Of course, those cases of reflection could have been avoided pre-emptively by type-hinting every little thing. As will be possible with Clojure 1.12's new syntax also. But history shows we take the idiomatic path and perfection throughout a multi-part system is very difficult to achieve.

seancorfield00:02:28

@U0HG4EHMH Most library maintainers will prioritize bugs that are actually stopping people doing work over fixing a warning (when the code still works). So, if those library maintainers decide to start using the 1.12 syntax and if an updated Java version or library happens to introduce a change that breaks the library, they'll be a lot more likely to fix it than dealing with "harmless" reflection warnings 🙂 (In other words, I doubt this will actually turn out to be a problem in the real world in future)

seancorfield00:02:54

And I don't recall anyone pointing this out but... ...library maintainers are not going to adopt 1.12-only syntax if they plan to support 1.11 or earlier... ...so this isn't even likely to become a real problem until libraries drop support for Clojure 1.11. Which might take years.

seancorfield00:02:16

(I haven't even looked at next.jdbc to see whether there's any code that might benefit from 1.12 -- because I still support 1.10 and have no plans to drop that any time soon)

seancorfield00:02:52

State of Clojure Survey 2023 -- Clojure versions still in use (a year ago):

oyakushev06:02:42

Also consider that post JEP261 the reflection is no longer as "harmless" and can cause breakages just as well.

seancorfield06:02:16

@U06PNK4HG Could you elaborate on that? JEP 261 is the module system stuff, right?

oyakushev07:02:41

Yes, which also introduced the concept of illegal cross-module access which was finally shut in JDK17. ... OK, I now see that I've said something stupid. Reflection in such case would be an issue only if you try to invoke a private method, and if you try to do that, then you already have a problem regardless whether there is one matching arity or many.

Alex Miller (Clojure team)18:02:07

hey all, I just wanted to follow up and say that we appreciate all the feedback on this and it's given us some new ideas and we'll incorporate into the next iteration, thanks

❤️ 7
Carsten Behring17:02:18

I have a data structure which contains already "byte array", and wwant to use nippy to thaw/freeze it. But I get this.

[:nippy/unthawable {:type :serializable, :cause :quarantined, :class-name "[I", :content #object["[B" 0x2b04bb91 "[B@2b04bb91"]}]

p-himik17:02:32

Check out the docs of *thaw-serializable-allowlist*: > If thaw encounters a disallowed Serializable class, it will [...] return a safely quarantined object of form > {:nippy/unthawable {:class-name <> :content <quarantined-ba>}}. > - Quarantined objects may be manually unquarantined with > read-quarantined-serializable-object-unsafe!. > > There are 2x allowlists: > - *freeze-serializable-allowlist* ; Checked when freezing > - *thaw-serializable-allowlist* ; Checked when thawing > > Example allowlist values: > - (fn allow-class? [class-name] true) ; Arbitrary predicate fn > - #{\"java.lang.Throwable\", \"clojure.lang.*\"} ; Set of class-names > - \"allow-and-record\" ; Special value, see [2]

Carsten Behring17:02:12

I saw this. But this is a "Byte array", does this matter ? Not a class

p-himik17:02:40

A byte array has a class as well. You can get it with (class (byte-array 0)).

hiredman18:02:55

[B is the jvm type for byte arrays, [I is the jvm type for int arrays

Ingy döt Net18:02:57

https://clojuredocs.org/clojure.core/require makes no mention of :exclude but this seems to work as it appears:

(require '[clojure.set :refer :all :exclude [join rename]])

1
Noah Bogart19:02:20

to mimic alex: make an Ask about it?

Noah Bogart19:02:57

glad to help

🙌 1
tomd20:02:18

Note :exclude is for refer not require (per se) and it is in refer's docstring

Ingy döt Net20:02:58

But it's something you can use in (require ...) which I found out by accident (and expectation).

tomd20:02:33

So do you expect :only and :rename to be included in require's docstring too? I'm not saying I necessarily disagree with you, btw. But it would seem to imply you expect all of refer's params to be listed

Ingy döt Net20:02:12

:all falls in the same category and is mentioned.

Ingy döt Net20:02:26

Either that or the docstring should mention that all of refer's keywords are honored

Ingy döt Net20:02:08

not a big deal though...

tomd20:02:39

Yeah, I mean it's a pretty massive docstring, so I can't imagine anyone left it out purely for brevity. Seems preferable to mention it honors all keyword args rather than duplicating the mention of each individually.

1
nando21:02:17

I’m getting an error when attempting to start 1.12.0-alpha6 or 1.12.0-alpha7 at datomic.promise$settable_future$reify__12002.get(promise.clj:45) at clojure.core$deref_future.invokeStatic(core.clj:2317) at clojure.core$deref.invokeStatic(core.clj:2336) at clojure.core$deref.invoke(core.clj:2323) This block is repeated multiple times until startup fails. Not sure where to report things like this?

seancorfield21:02:33

This was reported in another thread a few days ago... let me find the link...

ghadi21:02:47

this is a known issue. There is an incompatibility with alpha6 and 7 with Datomic

ghadi21:02:21

I’ve already fixed it internally but it will probably be rolled back / changed in alpha8

👍 2
Alex Miller (Clojure team)21:02:39

haven't looked at this one yet, we will at least roll it back