clj-kondo

grahamcarlyle 2024-11-19T15:33:52.170039Z

After picking up the new version of kondo (2024.11.14) and encountering the new linter :redundant-nested-call i'm wondering if its right to complain about threaded calls. I've got cases where i want to make separate calls to emphasise a difference.

$ clj-kondo --lint - <<< '(-> {} (merge {:b 2}) (merge {:c 3}))'
:1:8: info: Redundant nested call: merge
linting took 11ms, errors: 0, warnings: 0
vs..
$ clj-kondo --lint - <<< '(-> {} (merge {:b 2} {:c 3}))'
linting took 9ms, errors: 0, warnings: 0

borkdude 2024-11-22T13:51:06.839999Z

I think performance of comp, concat, every-pred, some-fn should still be verified since those are fixed + varargs

borkdude 2024-11-22T14:12:04.100789Z

user=> (time (dotimes [i 100000000] (let [f (comp inc inc inc inc)] (f 1))))
"Elapsed time: 2560.654917 msecs"
nil
user=> (time (dotimes [i 100000000] (let [f (comp inc (comp inc (comp inc inc)))] (f 1))))
"Elapsed time: 998.339292 msecs"

๐Ÿ˜ฎ 1
borkdude 2024-11-22T14:15:27.876329Z

user=> (let [l1 '(+ 1 2) l2 '(+ 1 2) l3 '(+ 1 2)] (time (dotimes [i 10000000] (doall (concat l1 l2 l3)))))
"Elapsed time: 3636.4735 msecs"
nil
user=> (let [l1 '(+ 1 2) l2 '(+ 1 2) l3 '(+ 1 2)] (time (dotimes [i 10000000] (doall (concat l1 (concat l2 l3))))))
"Elapsed time: 2059.557875 msecs"

borkdude 2024-11-22T14:16:20.610189Z

I think similar results will happen for the others, so perhaps it's better to remove those

tomd 2024-11-22T16:49:01.162889Z

curious. I wonder if the core team think this is rarely enough a perf bottleneck that nesting is an acceptable solution? couldn't find anything on ask.clojure so maybe it hasn't bothered anyone enough?

borkdude 2024-11-22T16:50:46.467069Z

I guess the answer will be: it's context dependent. But given that it's not always better to unnest and unnesting is mostly cosmetic I think clj-kondo should err on the cautious side

tomd 2024-11-22T17:00:54.198989Z

yup, completely agree

borkdude 2024-11-19T15:38:10.710719Z

yeah, I guess it's best to just disable this new linter for now then

borkdude 2024-11-19T15:39:39.311609Z

if you're calling merge with a one-sized map, why not just call assoc?

grahamcarlyle 2024-11-19T15:40:10.348549Z

sorry this was just to produce a minimal example, not the real code

grahamcarlyle 2024-11-19T15:42:46.270029Z

the real code looks more like

(-> ri
    ...
    (merge (select-keys enrichment-data [:product-code :software-a :instance]))
    (merge (select-keys (get arn->enrichment arn) [:applicable-to :size-flexible :normalization-factor]))
    ...)

Stig Brautaset 2024-11-19T15:43:13.080179Z

You can disable this specific linter at the call site. (Or globally, if this is something you do often.)

โค๏ธ 1
Stig Brautaset 2024-11-19T15:44:05.657019Z

IMO it's sensible to enable this by default though, as I imagine the counterexamples are much rarer than the real issues it finds.

grahamcarlyle 2024-11-19T15:47:22.747859Z

fair enough, i was thinking that if a value is being threaded then what is being interpreted as a redundant nested call is not as its something the author had intentionally called separately. But if that's not how others see it then i will make a change to our config.

borkdude 2024-11-19T15:49:30.262119Z

Feel free to leave some feedback here: https://github.com/clj-kondo/clj-kondo/issues/2431

borkdude 2024-11-19T15:49:41.987799Z

I'll generalize the issue a bit

grahamcarlyle 2024-11-19T16:08:05.900399Z

ok, i'm still ruminating on whether the linter is right here, as i think some readability will be lost following it but it does guide you to better performance ๐Ÿ˜„

scarytom 2024-11-19T17:24:16.216769Z

Is something like this both clearer, more performant, and linter compliant?

(let [product-enrichment (select-keys enrichment-data [:product-code :software-a :instance])
      arn-enrichment (get arn->enrichment arn) [:applicable-to :size-flexible :normalization-factor])]
  (-> ri
      ...
      (merge product-enrichment arn-enrichment)
      ...))

๐Ÿ‘ 1
grahamcarlyle 2024-11-19T17:33:42.985299Z

I think thats missing the point I was trying to convey, so apologies for my bad example. I'll try again with an example that intentionally threads twice because those steps are logically different...

(-> person
    (merge address-details)
    (merge billing-details)
    (assoc :account :premium))
vs
(-> person
    (merge address-details billing-details)
    (assoc :account :premium))
so i don't think its about let bindings, its about whether the performance hit of the nested merge is more important that the flow of change

scarytom 2024-11-19T17:48:15.299869Z

I think the latter example is perfectly clear if the terms of the merge are extracted and named like this

grahamcarlyle 2024-11-19T18:07:15.718059Z

Yes it probably is but my point is that logically distinct steps are collapsed due to the coincidental common implementation. If I factored those steps out to functions then I assume the linter wouldnโ€™t complain. So I think the linter is prioritising performance, which is fine I guess if thatโ€™s the idiomatic style.

2024-11-19T21:03:53.337789Z

Ya, some linters are annoying, that's just the reality. Linters always grow to annoy you. Probably just disable this linter. I mean, this linter as a whole is really just stylistic,.there's no errors made here, it's not a recognized best practice, etc.

borkdude 2024-11-19T21:04:39.678509Z

yep. Initially I was more enthousiastic about it than I am now. I might just disable it in the next release

๐Ÿ‘ 1
borkdude 2024-11-19T21:04:57.041169Z

also considering the perf regression it can cause (see issue)

tomd 2024-11-19T22:27:06.254609Z

merge was the one function that, when I saw it flagged in the linting output for the test corpus, felt there might be some pushback. Probably should have raised that in the PR. Seeing as this linter is primarily stylistic, I wonder if adding logic to exclude redundant nested calls inside threading macros could be worthwhile? otherwise, agree disabling by default might be best.

borkdude 2024-11-19T22:29:19.872669Z

FWIW, I did like the unnesting of and/or that I found here: https://github.com/babashka/sci/commit/fd0cb735c8b332ec4c6603e7da8b69c01be96fad

๐Ÿ™‚ 1
borkdude 2024-11-19T22:29:58.790769Z

also str in the above

borkdude 2024-11-19T22:31:20.020619Z

thread feels like something that could be relevant, as in, only unnest when it's positionally nested

๐Ÿ‘ 1
borkdude 2024-11-19T22:36:39.860529Z

like, if you wrote literally (merge a (merge b c)) probably nobody would object against the linter telling you about that right

๐Ÿ‘ 1
borkdude 2024-11-19T22:37:37.132939Z

while (-> (merge a) (merge b) (merge c)) is visually pretty apparent already, so yeah this makes sense to me

๐Ÿ’ฏ 1
borkdude 2024-11-20T08:47:52.776609Z

I added this feedback here: https://github.com/clj-kondo/clj-kondo/issues/2431#issue-2665689772

๐Ÿ‘ 1
tomd 2024-11-20T13:57:48.920889Z

> also considering the perf regression it can cause (see issue) as mentioned in the issue, there shouldnโ€™t be any perf regression. hopefully the issue will become just a tracker for the issue of nesting directly inside threading macros, which is still a fair point

borkdude 2024-11-20T14:01:19.688339Z

The issue does mention a few valid perf regressions

๐Ÿ‘€ 1
tomd 2024-11-20T14:10:14.086249Z

Am I missing something? Once you forget about < > etc I can't see anything..?

tomd 2024-11-20T14:10:47.514209Z

(see my comment on the issue)

borkdude 2024-11-20T14:41:39.215109Z

Aaah sorry, thanks for the clarification.

๐Ÿ‘ 1
borkdude 2024-11-20T14:41:56.959739Z

Maybe we should add a test that it doesnโ€™t warn for those, to make it future proof

tomd 2024-11-20T14:46:32.124149Z

Good idea. Will take a look

tomd 2024-11-20T15:39:20.269889Z

on second thoughts, no one is going to nest those fns, certainly not without getting a :type-mismatch lint error. because < and friends produce booleans, but expect numbers. e.g.

(< 1 (< 2 3) 4)
is the equivalent of doing
(< 1 true 4)
so my initial thought is this doesnโ€™t even need a test added

tomd 2024-11-20T15:40:46.808009Z

(lint! "(< 1 (< 2 3) 4)" conf/default-config)
;; => ({:file "<stdin>", :row 1, :col 6, :level :error, :message "Expected: number, received: boolean."})

borkdude 2024-11-20T16:19:18.098169Z

Good point :)

๐Ÿ™‚ 1