convex
convex copied to clipboard
Settling on predictable and consistent coercions
The recent work done on avoiding unnecessary implicit casting in functions has provided a lot of clarity (eg. avoiding (+ \a true)).
However, the coercion functions themselves are not there yet in my opinion. Four things to consider :
- Ensuring consistency in errors. For example, this string cannot be used as a hexstring (odd length):
(address "123")
;; ARGUMENT: String not convertible to a valid Address: 123
(blob "123")
;; Cast error: Can't convert argument at position 1 (with type String) to type Blob.
;; Both should be either :ARGUMENT or :CAST
-
Make a distinction between wrong type and wrong argument. I guess it would be best to keep the current (but inconsistent) distinction between
:ARGUMENTand:CAST. That(blob "123")error is misleading because it looks like no string at all can be converted to a blob. Ideally there should be enough information attached so that the consumer can understand why. Programmatically, so that an environment like the sandbox can humanize it as needed. -
Avoid non-obvious double coercions. This is more of a opinion but those are examples of situations that looks a bit weird:
(char true) ;; bool -> long -> char
(double \a) ;; char -> long -> double
- Allow double coercions when they look completely transparent. Especially in collections:
;; Given that the following works:
(vec {:a :b})
(vec 0x1234)
;; The following should work (but currently don't)
(set {:a :b})
(set 0x1234)
;; And should be consistent with `into` which should accept any `Countable`
(into [] 0x1234) ;; Doesn't work, but looks like (vec 0x1234)
Another example:
(blob-map "45" 45)
;; Cast error: Can't convert argument at position 1 (with type String) to type Blob.
(assoc (blob-map) "45" 45)
;; ARGUMENT: Cannot assoc value - invalid key of type String
I've created a draft CAD for error handling: https://github.com/Convex-Dev/design/blob/main/cad/errors.md
CAD improvements and/or PRs to improve consistency welcome!
(good stuff like I said :) )
And how do you feel about things like (char true) ?
(char true) should die for sure! That's truly an abomination.
I think a single level explicit cast Number -> Something or Something -> Number is probably fine, but Something -> Number -> Something is probably wrong.
Agreed, but honestly I even hesitate for stuff like (double \a) (Something -> Long -> Double).
Can you think of a good way to visualise all the possible coercions? I am thinking of some sort of 2D table "Cast function <-> Type". Maybe we can build programmatically and eyeball the results.
For scalar values, I guess that anything that can map to a number usually map to an integer, more precisely. Because those are usually finite sets of countable entities (booleans, chars, ...). So I would forbid any direct (double x) conversion but for actual numbers. If really want to end up with a double, do (double (long x)). It also implies the opposite, you cannot do (char 97.0) but must do (char (long 97.0)). This already simplifies quite a lot I find. You still have the freedom to do explicitly any crazy conversion that you heart desire and at least, you know what you are doing.
For real collections ('() [] {} #{}), I would expect conversion from any coll to any coll.
More than a year later, I still reached the same conclusion.
(char false) feels weird.
I have tightened the char handling specifically. These coercions are a little complex because:
CAST = wrong argument type provided
ARGUMENT = type is potentially valid, but value didn't work (e.g. outside unicode range)
Logically, these are different things