Problem statement
clojure.core predicates (see terminology below) that expect the input to be a specific type (through type hints, interop, or coercion) throw exceptions instead of returning false when given input of the wrong type. Handling these cases requires additional effort on the part of the caller.
Discussion
It is not always possible to know the type of a given input. This can be handled in a couple different ways: predicates, instance? checks, multimethods, and protocols. (The latter two are roughly equivalent to cond branches of instance? calls).
Using predicates to make decisions based on the type or value of the input allow for natural and readable code, clearly communicating the intention of the code path. For example, (even? foo) is clearer in intent than (= 0 (mod foo 2)).
However, this is complicated by some predicates expecting that the input already be a given type, throwing an exception instead of gracefully returning false.
For example, using true? to check if a given input is true requires no additional effort because it is generic over all types. But using zero? to check if a given input is 0 requires first verifying that the input is a number (probably with number?), because clojure.lang.Number isZero(Object x) unconditionally coerces the input to a java Number (source line), which throws a ClassCastException:
(and (number? foo) (zero? foo))
This is not impossible, but it's an extra step and leads to writing (= foo 0) which is concise and won't throw exceptions when given a string or nil (even tho it is less performant).
Accounting
As of 1.12.5, there are 75 predicates in clojure.core. 14 require the input to be a specific type (through type hints, interop, or coercion), while the rest accept all types.
Requires specific type: zero?, pos?, neg?, integer?, even?, odd?, NaN?, infinite?, empty?, future-done?, future-cancelled?, realized?, and technically bound? & thread-bound? (these last two take varargs but otherwise act as predicates).
NOTE: I didn't include contains?, every? in my counts above. These take a collection and a value which disqualifies them as predicates, but they do require that the first arg be a collection or seq (respectfully) and throw otherwise.
Egregious Suggestion Zone
I know it's uncouth to suggest solutions without discussion up front, but this is my favorite part so please forgive me. Here they are in increasing difficulty:
- Do nothing. Always possible, easiest solution. We're N years into this and it's been fine, why fix anything?
- Change the doc strings to say that they throw if given incorrect types. Some of the doc strings already say this (
even?, odd?), but the rest could specify as well.
- Update the behavior to include instance checks before performing coercions or including type-hints.
For example:
public static boolean isZero(Object x) {
if (instance? Number x)
return Numbers.ops(x).isZero((Number)x);
return false;
}
Terminology
- predicate: a single-arg function that returns true or false given some feature of the input
See Also
Originating thread on slack about this