clj-kondo

2025-07-10T19:51:48.395649Z

i was reading the type-related code in clj-kondo.impl.types.clojure.core , and :seq not being a nilable confuses me. if i call (seq foo), i expect to get back a seq or nil, which to me says :nilable/seq. am i confused about how clj-kondo uses :seq as a return type?

borkdude 2025-07-10T20:28:19.151739Z

I think you are correct. Challenge: given this "bug", can you come up with a code snippet where this causes a false positive?

2025-07-10T20:30:46.084209Z

not yet lol

2025-07-10T20:31:53.468239Z

i suspect that :seq is already being used as :nilable/seq, where things that expect a seq intelligently deal with nils

2025-07-10T21:01:43.166349Z

the thing i'm thinking about is that :seq is overly broad, so you can't say "this function takes a non-empty sequence"

2025-07-10T21:02:28.905879Z

it's permissive so there's no issues right now, but i'd like to be able to say "this function errors if given an empty sequence" and then calling (foo (seq coll)) would say "hey, this might be mismatched"

borkdude 2025-07-10T21:03:30.982719Z

encoding non-emptiness in types isn't trivial right?

2025-07-10T21:04:16.889049Z

sorry, yes

2025-07-10T21:04:20.531179Z

wrong example

2025-07-10T21:04:28.537439Z

i mean the difference between (rest foo) and (next foo)

2025-07-10T21:04:56.095479Z

where (rest foo) will always return a truthy value, and (next foo) will return a truthy value or nil

2025-07-10T21:05:26.425329Z

but both of them are typed as returning :seq

2025-07-10T21:05:53.358139Z

(if (rest ()) :true :false)
; :true

borkdude 2025-07-10T21:06:12.797229Z

ah yes, I was just typing:

(if (rest '()) :a :b)

2025-07-10T21:06:17.090539Z

lol

borkdude 2025-07-10T21:06:20.911189Z

this should warn about always being truthy

2025-07-10T21:06:39.615329Z

rest can return nil

2025-07-10T21:07:00.012239Z

(if (rest nil) :a :b)
; :a

2025-07-10T21:07:34.160799Z

i'm interested to see rest return nil, i'm not sure how to do it

2025-07-10T21:08:01.381119Z

oh, do you have to write a custom deftype that returns nil in the more impl?

2025-07-10T21:09:34.776469Z

Sure, its a java method call that returns a reference type

2025-07-10T21:10:26.382299Z

can map or filter ever return nil?

2025-07-10T21:10:49.909559Z

The current impl won't

2025-07-10T21:13:52.153119Z

They try to be maximally lazily, and which dictates immediately returning some kind of delayed value, so not nil

2025-07-10T21:16:27.412699Z

But I think they certainly could be changed to return nil immediately without breaking the maximal laziness, just slightly more awkward code

2025-07-10T22:40:12.145319Z

well, for the purposes of clj-kondo, i think assuming rest and map and other core functions return truthy objects and seq returns a nilable object is worthwhile

2025-07-22T19:08:51.263189Z

> Not sure what you are trying to say, (lazy-seq (rest s)) in the above absolutely keeps a reference to s until it is realized I meant that in (defn foo [s] (lazy-seq (foo (rest s)))) it seems like a good idea for rest to be an eager operation so recursive calls don't hold the head of the seq.

borkdude 2025-07-11T14:48:37.076829Z

I always find it satisfying when something is fixed with tangible benefits. E.g. I now fixed an issue in SCI that I had in the backlog for two years and now I finally ran into a situation where it was worth fixing the issue. Perhaps I could have fixed it two years ago but at least I now have more understanding of why the fix is necessary... Something similar is going on here. I've never found an issue with the :seq type for things that expect nil or a seq... perhaps it's there though!

👍 1
2025-07-11T15:20:41.151469Z

i'll play around with it, see if this makes any difference

2025-07-11T18:48:51.497109Z

IMO rest returning nil violates documented behavior. and I think it can be argued that the rest abstraction is at least as eager as seq and just returning a delayed value without dropping the head of the collection is not the intended implementation. consider this

(defn foo [s] (<...> (lazy-seq (foo (rest s)))))
if rest was fully lazy, you'd hold onto the head of s. for chunked seqs, you'd hold onto up to 2 chunks at once instead of 1.

👍 1
2025-07-11T18:57:18.731369Z

I have a mildly convincing draft blog post somewhere. But this is my summary from my pov, from https://frenchy64.github.io/fully-satisfies/latest/io.github.frenchy64.fully-satisfies.head-releasing.html that improves the memory usage of core functions: > At the very least, this work helps crystallize the differences between rest and next---or more precisely, their similarities: while rest does not realize the next element, nor tell us whether a seq has more elements, it is still an eager operation whose result, like next, releases a strong reference to the first element of the seq.

2025-07-11T19:02:31.402409Z

I think in terms of least to most eager it's seq < rest < next. Though honestly I don't think I've really nailed the argument, so I've mostly just kept it to myself. Interesting stuff tho. I suspect rest < next since next is described as (comp seq rest) in places, so I think next must realize 2 elems, but rest only 1. But maybe seq <= rest?

2025-07-11T19:36:08.783479Z

Not sure what you are trying to say, (lazy-seq (rest s)) in the above absolutely keeps a reference to s until it is realized

2025-07-11T19:39:00.544539Z

In general this is why you don't see lazy seq ops in core defined in a way that do stuff before being wrapped by the lazy-seq macro

2025-07-11T19:40:35.281289Z

make consumers force the seq to get anything, and once the seq is forced the result can be cached any references that where held to compute that step.can be cleared