Welcome! Please see the About page for a little more info on how this works.

+1 vote
in Collections by
edited by

Just ran into some interesting behavior with sorted-set. Namely, the contract to the user for a normal set is nil when the element doesn't exist. However, with sorted-set, the call will throw if the comparator doesn't gracefully handle the type. The reason makes sense (the comparator requires the ability to sanely compare elements), but the interface is somewhat confusing in the "no membership" case since we know that the element doesn't exist in the set if its type can't be compared by default.

In other words, I guess I'm proposing that a sane default behavior for a sorted-set using the default comparator should be nil rather than throw when the type cannot be compared, in order to allow nil-punning.

As an example, this case was encountered when the form was passed to a keyword function (:foobar form), which allows nil-punning on a lot of other collections. Here's a small snippet:

user> (def normal #{"0000"})

;; => #'user/normal

user> (def sorted-version (sorted-set "0000"))

;; => #'user/sorted-version

user> normal

;; => #{"0000"}

user> sorted-version

;; => #{"0000"}

user> (:foobar normal)

;; => nil

user> (:foobar sorted-version)

Execution error (ClassCastException) at user/eval246355 (form-init59552761865518221.clj:8123).
class java.lang.String cannot be cast to class clojure.lang.Keyword (java.lang.String is in module java.base of loader 'bootstrap'; clojure.lang.Keyword is in unnamed module of loader 'app')

I think my options for handling this are either try/catch or write a custom comparator, and both seem a little wonky :)

I'm probably missing something, but thought it was interesting anyway :)

1 Answer

+1 vote
selected by
Best answer

If you have a sorted set of strings, asking whether it contains a keyword is an exceptional case. What should that even mean?

If you want tolerance for this, you will need to provide a comparator that provides a sort order over a broader set of things you might pass it.

Thanks for the response! A couple follow-ups:

  - Why is checking whether a keyword is contained in a _sorted_ set of strings any more exceptional that checking whether it's in a _normal_ set of strings? (Regardless of the fact that the implementation of a sort requires comparison, mostly curious from an API perspective why it's deemed exceptional to check membership in the _sorted_ case but not the normal case).

  - For that matter, why is `(:foo "bar")` not exceptional? What is that call supposed to mean?

Essentially, it seems to me that lots of things gracefully handle `(:foo coll)`, despite them not really making sense, but they don't throw exceptions and happily return `nil`. :)
Because being sorted means you have a comparator with implicit expectations about what you are able to sort.

`(:foo "bar")` is exceptional and it's behavior should be considered undefined
Fair enough :) I guess part of the confusion is that:

`(:foo "bar")` is exceptional, but does not throw an exception
`(:foo sorted-set-coll)` is exceptional, but _does_ throw an exception

The original problem was found from some code that was running postwalk and calling a keyword-fn on each form, which works fine for most datastructures (like strings).
perhaps a better word for these cases is "undefined"