Fork me on GitHub

For anyone familiar with the CLJS implementation of clojure.spec, do you have any insight into the difficulty of fixing this bug?


but I thought recursive specs worked fine, but maybe there was a regression


should get a test for that if true


I bet CLJS-2940 can be further constrained to things involving s/nilable (in other words, I think the ticket title incorrectly implies a broader problem than that exhibited in the description). I’ll update it.


My mistake. I tested with s/nilable and assumed that applied more broadly than it did.


were recursive specs ever documented anywhere?


I was unsure about when I could get away with them or not before I remember


I think mutually recursive was particularly interesting


I'm not sure about explicit documentation but there is s/*recursion-limit* so they are meant to be supported


ah, true there is that


yeah, I just always assumed they were supposed to be fine


mutually recursive was odd to write I belive in some cases


I’m going off on a tangent that is better suited for #clojure-spec channel now though, so nvm here.


I wonder if we could update cljs.core/merge-with to match clojure.core/merge-with, now that maps produce map entries. (Specifically changing first to key and second to val in the implementation.)


I think the previous change already forced people to stop relying on the old semantics


I’ll put together a ticket for it, with perf numbers. Then we can let it sit for a while for consideration / soaking.


Cool. Speedups in the ballpark of 1.3. Details in ticket.


Hah, here is an example of code this patch would break, which doesn't rely on the older use of 2-vectors as ersatz map entries, but simply on the fact that of first / second are being used in the merge-with implementation

(->> [:a 1 :b 2] (partition 2) (merge-with vector {}))
In the wild here TIL there is a name for this concept: Hyrum's Law


(Note that (partition 2) isn't producing vectors, but lazy seqs of length 2. 🙂 )


Captured this in the ticket

Alex Miller (Clojure team)20:10:18

eww (re using sequences and first/second for entries)


I guess, since Clojure and ClojureScript are "popular", Hyrum's Law states that someone will have discovered that first and second are hiding inside merge-with

Alex Miller (Clojure team)20:10:39

map entries should always be indexed / positional


Hyrum's Law also seems to imply you can't change anything in core without breaking someone.


This merge-with use also seems to rely on an undocumented aspect of conj that happens to work (I can't recall why, but it feels like it is an accidental internal implementation detail):

(conj {:c 3} (seq {:a 1, :b 2}))

Alex Miller (Clojure team)20:10:51

conj on maps expects something that seqs to map entries (which can be maps, but can also be just seqs of map entries)

Alex Miller (Clojure team)20:10:27

keys and vals have the same relaxed expectation

Alex Miller (Clojure team)20:10:33

user=> (keys (seq {:a 1, :b 2}))
(:a :b)

Alex Miller (Clojure team)20:10:22

I am increasingly running into cases where it would be useful to be able to state that something “seqs to map entries” (often in spec-related stuff)

Alex Miller (Clojure team)20:10:06

Rich and I have talked about it a couple times, maybe that will emerge as an abstraction at some point, dunno


Sounds like some of the subtleties you have run across while spec'ing core, I guess?

Alex Miller (Clojure team)20:10:39

yep, although I think I ran into it in the spec implementation, not in a spec


I applied shadow-cljs to myu project and it seems to work just fine, except for hot reload. It does watch my changes and recompiles but in my browser the changes are not visible and refresh also doesn't bring changes.


Figured it out 🙂

Alex Miller (Clojure team)20:10:22

“seqs to map entries” is weaker than map? but still sufficiently interesting to be useful


@mfikes yeah I think people should fix stuff like that, seems gratuitous?


If those use cases you mention are by design, then "seq to map entries" definitely sounds like it could use a name in spec.


This is a completely new concept to me. TIL that seq? of map-entry? has meaning.


@dnolen Yeah, for that particular case I'll probably file a ticket against the downstream repo.

Alex Miller (Clojure team)20:10:44

@andy.fingerhut the problem is that there is no easy way to write that predicate

Alex Miller (Clojure team)20:10:53

without actually forcing the seq


Do you consider it a requirement, or just a really really good idea, for a spec not to force a seq?


I gotta say, this is totally badass. I never knew:

(keys (filter (comp pos? val) {:a 0, :b 1}))


more interestingly, the example from secretary uses this: (->> [:a 1 :a 2 :b 3] (partition 2) (merge-with vector {})) ;;=> {:a [1 2], :b 3}, so you cannot simply replace (partition 2) with (apply hash-map) to fix it. But converting the 2-colls to map-entries will make it work with the patch.


> conj on maps expects something that seqs to map entries (which can be maps, but can also be just seqs of map entries) Perhaps instead of "seqs to map entries" it is "is a seq of map entries"? (Compare (conj {:c 3} (vec (seq {:a 1, :b 2}))) and (conj {:c 3} (seq (vec (seq {:a 1, :b 2})))))


the minimal case is just (vec {..}) vs (seq {..})/`{}` FWIW


but yeah, the "problem" here is that vectors take the path of a map entry, rather than that of a collection of map entries

👍 4

the polymorphism of conj on maps has always been a weird edge-case IMO

👍 4

Yeah, and it is specific to conj because keys and vals are OK with it.


the spec according to the impl would be: conj on maps takes either a map entry or a vector of size 2 representing a map entry, or something that seqs to map entries which is not a vector

👍 4

Indeed. I agree.


Regardless of spec, an interesting question is whether the docstring for conj should essentially say what you just said. 🙂


Maybe even: "conj on maps takes either a map, a map entry or a vector of size 2 representing a map entry, or something that seqs to map entries which is not a vector"


>> conj expects another (possibly single entry) map as the item, and returns a new map which is the old map plus the entries from the new, which may overwrite entries of the old. conj also accepts a MapEntry or a vector of two items (key and value)


it never mentions acceptiong anything that seqs to map entries


Oh, shit. I didn't think to look there. Thanks.


and I suspect that the implementation accepts it just because that was the easiest way to implement map+map merging


hard to say


That's what I thought. It was an internal accident of implementation based on ease of implementation. But Alex's comment is that it is instead intended to be supported, not as an accident.




maybe it simply graduated from accidental to not accidental as the rationale got lost in time, but the edge-cases never got ironed out?


I'd buy that.


ClojureScript master's Graal.JS support is still good with the recent GraalVM 1.0.0-rc8 release.