should `the` should perform coercion?
scolsen opened this issue · comments
Our the
form currently doesn't coerce values on its own--it only enforces that some value t
is of a given type:
(Int.+ (the Int \a) 1)
I can’t match the types `Int` and `Char`.
(the Int \c) : Int
At line 3, column 8 in 'REPL'
At line 3, column 17 in 'REPL' at REPL:3:1.
But this sort of check will be performed by the type checker anyway, without the the
form.
To actually coerce a value, one needs to combine Unsafe.coerce
and the
:
(Int.+ (the Int (Unsafe.coerce \a)) 1)
98
Now, there are pros and cons to this approach:
- pros
- it's very clear what's going on, and the additional verbosity helps ensure programmers avoid mistakes
- coerce is unsafe, which communicates that the operation may cause a runtime error
- cons
- this is much more verbose than only writing a single
the
or singlecoerce
the
by itself seemingly does nothing
- this is much more verbose than only writing a single
So, I'd pose the question, should we make it so that the
performs the coercion? Or should we drop the
altogether. in favor of a more complex coerce implementation that takes a direct type argument (Unsafe.coerce Int \a)
?
I feel like the
is like a type annotation in e.g. Haskell—a necessary evil to cope with the shortcomings of type checking. I know that sometimes you need it, for instance when defining prn
based on str
for a type:
(deftype X [])
(defmodule X
(defn prn [x] (str x)) ; x is generic here!
(defn prn [x] (str (the X x))) ; this makes sure the type checker knows
(defn prn [x] (X.str x)) ; the alternative
)
I don’t think overloading it to also do conversion is sensible, since I think it actually does the opposite: it ensures something is not of any other type, whereas coerce
ensures that any other type will be made to conform.
Does that make sense?
I feel like
the
is like a type annotation in e.g. Haskell—a necessary evil to cope with the shortcomings of type checking. I know that sometimes you need it, for instance when definingprn
based onstr
for a type:(deftype X []) (defmodule X (defn prn [x] (str x)) ; x is generic here! (defn prn [x] (str (the X x))) ; this makes sure the type checker knows (defn prn [x] (X.str x)) ; the alternative )I don’t think overloading it to also do conversion is sensible, since I think it actually does the opposite: it ensures something is not of any other type, whereas
coerce
ensures that any other type will be made to conform.Does that make sense?
Yes, it does! Thanks for clearing that up.
So it's essentially only forcing concretizations on its lonesome, but in combination with coerce, can do more, makes sense!
You can achieve the same thing with functions using, sig
too. That's how I usually do it:
(deftype X [])
(defmodule X
(sig prn (Fn [X] String)) ; this makes sure the type checker knows
(defn prn [x] (str x))
)
I guess a follow up question would be, do we need both sig
and the
? The flexibility is pretty nice since (the X x)
is shorter than adding a signature and you may only want to force on particular part of a function, keeping the rest generic (though this is achievable with sig as well).
edit: fixed a couple errors -- typing too quickly 🙃
I’ve found myself having to use both sig
and the
in the past. That’s probably a bug, but until that’s fixed, we need both! :)
I have found myself having to locally annotate expressions in big functions as well, though I can’t find any examples right now.
A good example for a small function that lives and dies with the
is Vector.last
from dimensions:
(doc last
"Returns the last element of a Vector.")
(sig last (Fn [(Ref (Vector (Size n) a))] a))
(defn last [v]
@(Array.unsafe-nth (Vector.elements v) (to-int (the (Phantom n) (Phantom.init)))))
It has a sig
, but needs a local the
anyway.
Good examples! Definitely seems like we need it right now. I do think some of the sig+the cases are probably bugs. At any rate it's probably good to have the flexibility of both forms regardless, so I think we can close this.
Thanks for the discussion!