clojure 2026-07-03

Another discovery that I certainly should have been aware about earlier:

(map? (sorted-map 1 1))
;; true

(contains? (sorted-map 1 1) :hello)
;; throws  java.lang.Long cannot be cast to class clojure.lang.Keyword
What makes it surprising for me is that, apparently, not every Associative/`ILookup` collection is safe to make arbitrary lookups in. If I don't know what kind of map I receive and I need to check a key in it, is it sufficient to filter out sorted? , or should I protect every such check with a try/catch?

One should not be surprised that not all maps support lookup (or insertion) of arbitrary values. As you've seen, sorted collections require comparability of elements. It's not an idiosyncrasy, they can't work otherwise. In order to support lookup of anything, they'd need a universal comparator that can take all known and not-yet-known types and provide a total order for them. You can use sorted-map-by to supply your own comparator if you want to try building a universal one. Have a look at https://clojure.org/guides/comparators. Alternatively, get et al could catch exceptions during lookup and turn them into not-founds. Such a "safe" lookup is an anti-pattern in most applications since normally people know what their collections are and looking up or inserting e.g. a number in a sorted collection of strings is a bug in their program they'd rather know about rather than get silent failed operations.

πŸ™ 1

Thank you for the answer, Rich! I fully understand the design decisions made. I was under a false impression that get is safe to call on all maps, especially given how lenient it can be in other cases (e.g. (get "hello" :a)), but it gave me a reminder to be extra careful when dealing with unknown data structures.

πŸ‘ 1

Oh yeah, it's ancient stuff. I'd say you can't 100% rely on just sorted? - not if you might have to deal with custom map types.

user=> (require '[ham-fisted.api :as h])
nil
user=> (def l (h/double-array-list))
#'user/l
user=> (.add l 1.0)
true
user=> (get l 0)
1.0
user=> (get l :a)
Execution error at ham_fisted.Casts/longCast (Casts.java:85).
Object cannot be casted to long: :a

At least, (map? l) is false. But it is an ILookup.

Ah, right you are. I was looking a ILookup since that's what get uses.

yes, this bit me a couple of times. some projects I work on have a safe-get

βž• 1

Is there a better analogue to Java's https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html than (let [x ,,] (try ,, (finally (release x))))?

2
βœ… 1

A more meaty example from today's work:

(let [watcher (object-store/watch connection watch-store-name on-change {:on-end-of-data on-end-of-data})]
  (try
    (object-store/put-str connection watch-store-name "hi1.txt" "hi there")
    (object-store/put-str connection watch-store-name "hi2.txt" "aloha")
    (finally
      (object-store/unwatch watcher))))

I'm mostly interested in clojure.core; I don't think I'd want to write or pull in a macro for this.

wait, is this just with-open? πŸ€” πŸ¦† ❓

I think this is just clojure.core/with-open πŸ˜„ As long as as java.lang.AutoCloseable is implemented on the type.

πŸ‘ 1
πŸ‘πŸ» 1
πŸ‘πŸΌ 1
πŸ˜‚ 2

yup, just AutoCloseable and with-open.

(with-open [_ (reify java.lang.AutoCloseable
                (close [_] (prn "closing!")))]
  (prn "open!"))
prints
"open!"
"closing!"
to stdout.

If some object is intended to be working with try-with-resources, it should already be implementing AutoCloseable, so there should be no need to use it directly. And if it doesn't implement it, you can use a slightly more concise java.io.Closable (which also extends AutoCloseable).

I'm working on a library here, so I'm responsible for the data model, and whether my types implement AutoCloseable.

Is there a semantic difference between AutoCloseable and java.io.Closable?

Closeable is more for dealing with IO interfaces

Is just the Exception that it emits

close () method of closeable throws IOException, Autocloseable's close()method throws Exception

@d.ian.b thanks for the explanation! πŸ™

πŸ‘Œ 1

Is there a way to hash-pin dependencies in deps.edn?

Alex Miller (Clojure team) 2026-07-03T18:30:52.511759Z

No, other than just specifying all deps and versions at the top level (only relevant for applications, libs never control their actual deps)

Is there a reason that this doesn't exist while lock files are common in other ecosystems?

Because if you depend on x.y.z, it will always be that and not some x.y.f(z). Consequently, all transitive deps will also be locked automatically. Unless someone releases the same version with a different content, or depends on a non-specific version like RELEASE or x.y.z-SNAPSHOT, both of which are heavily frowned upon. The former might even be disallowed now in some registries - I remember hearing some talks about it but no clue whether anything came out of it.

☝️ 1

Locking down versions is also much less important if new versions aren’t constantly breaking things. For the rare occasions when you do want specific deps, you can list them as top level deps.

Alex Miller (Clojure team) 2026-07-03T19:44:35.958879Z

Things work differently in Clojure largely because they follow the traditions of the Java/Maven ecosystems. How and why that tradition is different than the lock file / pinning one is a historical/cultural question that I do not know the story

I guess I'm curious what problem you think you need to solve @alex.sheluchin? Or is the question more curiosity?

Some ecosystem (hi, node.js) introduced lockfiles because initially they were without and all was initially designed to embrace always the latest&shiniest of all deps (semver allthethings!), but (to put it politely) it didn't play well on the long run. I honestly ignore the gnarly internals of deps resolution on Clojure (thinking both deps and lein) but what is provided (with overrides and the sorts) is largely sufficient to prevent the madness that was something like node.js without a lockfile. Then for sure I cannot exclude that there might be edge cases which I haven't stumbled on. Actually the talk https://www.youtube.com/watch?v=sStlTye-Kjk by our @alexmiller goes in depth on what was missing on Clojure ecosystem at the time on this front (but I should watch it again because it was some time ago 😁). Since our Clojure overlords spoil us with really high standards of quality and thoughtfulness in any design decision, I'm going to presume that what is provided is solid and there's no need to mimic the lockfile convention (that in other ecosystem is there precisely because the first version of things was fundamentally broken by design). ... unless you actually stumbled on an edge case or bug (rephrasing a bit the first question by @seancorfield) in which case it would be important to know. PS.: we are also spoiled by clojure.core itself - you might need 10-15 dependencies in other ecosystems to match it. Plus, add Java interop and that covers for another 120 dependencies 😁 sorry for bragging, it's not my fault if Clojure ecosystem is the absolute best by a ridiculously large margin. (plus add the rock solid culture of no breaking changes, etc)

πŸ’œ 1

writing the above brought up some, um, good old memories (not that a lock file would have helped there, but is something else where things were already designed better in the rest of the non-npm world, eg Maven) https://en.wikipedia.org/wiki/Npm_left-pad_incident

I'm pretty sure on Central and Clojars you can't override the same version, so once published it wouldn't change. On central at least you cannot change, delete or update a published version of a library. And you cannot publish a library that depends on a dynamic version of a dependency say using -SNAPSHOT or RELEASE.

Interestingly, it seems Clojars doesn't block dynamic dependencies. That means it's at risk of supply chain attacks

@didibus would you mind filing an issue for that at https://github.com/clojars/clojars-web/issues? I'd be happy to discuss possibly adding that as a validation.

πŸ‘ 1

Also, to answer: > I'm pretty sure on Central and Clojars you can't override the same version Correct; you can't overwrite a non-SNAPSHOT version on either repo.

@seancorfield I'm looking for ways to protect myself from supply chain attacks.

iirc you can dump the resolved deps as edn then put them all in your deps.edn

ways to protect myself from supply chain attacks
probably the best bet is targeting JARs (and Maven reliability on immutable packages, etc) and not git commits, which is what most people do in non-pet projects (e.g. work). But thanks for raising this, exactly because in my usage I almost exclusively targeted jars/uberjars (with a sprinkle of :override-deps & co where necessary) this wasn't even under my radar.

Hello I am trying to test some code that generates code as a list (so quoted). For other reasons it is not a macro at the moment. I want to verify that the resulting list is as expected, but checking equality with clojure.test is not working. This is an example:

(deftest modify-body-test
  (let [params {:out 0}]
    (is (= '(#'overtone.core/out 0 (#'overtone.core/sin-osc))
           (modify-body
            params
            (qualify-body params '(o/out 0 (o/sin-osc))))))))
I am getting this results:
Results

tieminos.sc-utils.synths.template-synth.v0-test
1 non-passing tests:

Fail in modify-body-test

expected: (#'overtone.core/out 0 (#'overtone.core/sin-osc))
  actual: (#'overtone.core/out 0 (#'overtone.core/sin-osc))

    diff: - [#'overtone.core/out nil [#'overtone.core/sin-osc]]
          + [#'overtone.core/out nil [#'overtone.core/sin-osc]]
Looks like everything should be alright, but it seems like it is not recognizing the vars as the same. In fact if I cast to string I get this:
expected: "((var overtone.core/out) 0 ((var overtone.core/sin-osc)))"
  actual: "(#'overtone.core/out 0 (#'overtone.core/sin-osc))"
Any suggestions?

the pretty printer is probably printing (var overtone.core/out) as #'overtone.core/out. Without looking at modify-body, my guess is that expected has a list (var overtone.core/out) whereas the actual is an actual instance of clojure.lang.Var

but both print to the same thing

ah gotcha

> (with-out-str
    (clojure.pprint/pprint (list 'var 'foo/bar)))
;; "#'foo/bar\n"

Think I can just replace the parenthesis in the output to vectors. Seems like the quote is intefering with the #'

This works:

(deftest modify-body-test
  (let [params {:out 0}]
    (is (= [#'overtone.core/out 0 [#'overtone.core/sin-osc]]
           (modify-body
            params
            (qualify-body params '(o/out 0 (o/sin-osc))))))))

If you're generating code, the generated code shouldn't contain instances of clojure.lang.Var

Though if there’s a better solution it’s be nice.

my intuition is that qualify-body is not doing the right thing. it should probably be generating a list like (list 'var fully-qualified-symbol) rather than a var

Are you using syntax quote?

or maybe just generating the fully qualified symbol

I am using var-get to convert o/something to overtone.core/something

you would normally do something like:

`(o/out 0 (o/sin-osc))
which will fully qualify the symbols

you can also write

`o/out
to get the fully qualified symbol conveniently

> (let [a-number 42]
    (list `o/out a-number `(o/sin-osc)))
;; (overtone.core/out 42 (overtone.core/sin-osc))

I see, yeah, perhaps my implementation could be improved. Is there a function that does the same as

`

You can resolve a symbol in the current namespace, which gives you a var, and then look at the namespace and name of the var. There's probably a more straightforward approach. It might be worth taking a step back and explaining what you're trying to do.

Overtone has some macros to define Supercollider synths. I wrote code to be able to modify the synth body so that is possible reuse parts of the code that define the body. All those :ugen/something are placeholders for β€œplugin” code. (That is not possible in SuperCollider or Overtone, and it may be of limited use, but I do have those needs often.) At some point I do call the macro using eval.

(eval (list 'overtone.core/synth
                              (instance-symbol namespaced-synth-string synth-symbol)
                              params
                              ugen-form))
I know eval is discouraged, and the whole idea might be horrifying, but it does the trick, and I guess it’s not unsafe for my purposes. The code actually works, but I am adding tests for stability and documentation within my codebase.

At first I did try to do everything with macros, but I simply couldn’t figure it out, this has been my best try so far.

Lisp folks love to talk about how awesome macros and eval are and then shame anyone for using them. I say if you're learning and having fun, then go for it. Obviously, just be a little bit empathetic for anyone who might have to use the code.

Yeah, I don’t think anyone would want to. The API is decent though.

πŸ‘ 1

sometimes, libraries have a convenient macro interface, but also a lower level interface that is easier to use programmatic. I don't know off the top of my head if overtone has a less macro heavy programmatic API

there is an #overtone channel if you're interested.

here's some sample code that might help:

(let [v (ns-resolve (the-ns 'clojure.core) 'filter)
      ns (.ns v)
      sym (.sym v)]
  (symbol (name (ns-name ns)) (name sym)))

πŸ™ 1

I don't have overtone loaded, so I used a function clojure.core as an example.

this will give you a fully qualifed symbol

I do participate in that channel. And did spend a lot of time reviewing that part of the covertone codebase. There’s a lot of macros that call other macros, so going for a lower level approach would have meant redoing a lot of things. Using their macro, was the most cost-efficient thing I could think of doing, and it does really well, I can define new synths on the fly.

Thanks for that sample.

you can use ns-resolve to lookup vars using aliased symbols as well:

;; in user ns
(require '[clojure.string :as str])

(let [v (ns-resolve (the-ns 'user) 'str/split)
      ns (.ns v)
      sym (.sym v)]
  (symbol (name (ns-name ns)) (name sym)))
;; clojure.string/split

Thanks!