(t/defalias Tokens (t/HMap :mandatory {:access_token t/Str
:refresh_token t/Str}
:complete? true))
(t/ann get-tokens [:-> (t/Option Tokens)])
(defn get-tokens []
(let [res (prepare-execute-one!
"SELECT access_token, refresh_token FROM token_config" [])
at (:access_token res)
rt (:refresh_token res)]
(when (and res (string? at) (string? rt))
{:access_token (str at)
:refresh_token (str rt)})))
Is there a better way to type this function? prepare-execute-one! returns a (t/Option (t/Map t/Kw t/Any)) since that is hitting the db, but I would like the get-tokens function to return the Tokens structure. I am just curious if this is the best way to do this while satisfying the type checker.I've been messing around a bit more with functions like this and what about if I have a query returning a lot of fields. Doing something like this is kinda of unruly.
(if (and (string? at) (string? rt) (string? v1) (string? v2) (string? v3) ...)
{:access_token at
:refresh_token rt}
(throw ...))
It would be nicer if I could do this.
(if (every? string? [at rt v1 v2 v3 ...])
{:access_token at
:refresh_token rt}
(throw ...))
However, this doesn't satisfy the type checker is that due to the same kind of bug with inference or is it something else i'm not understanding?Could be related, did you use tc-ignore to bind the variables?
Yeah I did the same tc-ignore on all of them in the let
I think in theory it should work looking at the type of string? and every?. That's unfortunate that it doesn't :(
Ok shall I add that example to the bug report or is this more of an unimplemented feature you think?
It should work, open a different ticket for it.
Possibly related to these two inference bugs with HMaps, I noticed this recently.
(t/ann sum-cents [(t/Vec (t/HMap :mandatory {:cents t/Int}
:complete? true)) :-> t/Int])
;(defn sum-cents [m]
; (reduce + (map :cents m)))
; => Does not type check
(defn sum-cents [m]
(reduce + (map (t/inst :cents t/Int) m)))
; => Type checks
;(defn sum-cents [m]
; (reduce + (map (t/inst :incorrect-key t/Int) m)))
; => Does not type check
In this example, I would expect the first commented out version to type check since we know the type of the input argument must be a vector of maps with a key cents that must be an integer. If I explicitly use t/inst on the keyword it proceeds to type check. However, if I misspell the keyword then it doesn't type check. That leads me to believe that the checker knows what the correct type of :cents on the map is already. Unless it just knows the correct key name, but I am unsure why it wouldn't be able to infer the type as well since the keywords match and since it is checking for that.Passing polymorphic functions to other polymorphic functions is currently too hard for type argument inference. Curious does (map #(:cents %) m) work?
That type checks but it gives a warning "Keyword lookup gave bottom type: :cents t/Nothing"
And I get it for some other polymorphic functions but in the case of keywords doesn't the type checker have that info already since its matching keywords to the ones in the HMap definition already?
Hmm the warning probably means (map #(:incorrect-key %) m) type checks too?
Thats the weird thing I checked that too but that one fails
Keywords are polymorphic functions, and there is no special treatment of them in higher-order contexts.
Oh! Perhaps the function is checked twice.
There's a bit about how higher-order functions are checked here https://github.com/typedclojure/typedclojure/blob/main/example-projects/symbolic-guide/test/typed_example/symbolic_guide.clj
I think that warning should only fire if the input to the keyword is not bottom.
I think this should fix the warning https://github.com/typedclojure/typedclojure/commit/1a13628b27e05e5fb3c50c53214f4b2e778adb2b
But anyway, these two expressions type check in the same way:
(map (t/ann-form (fn ...) (All [x] [(HMap {:a x}) -> x])) ...)
(map :a ...)Ok thanks Iโll check out that guide to try to understand the library some more. And I appreciate the patch for the warning. Iโm still a little confused how :a doesnโt type check but #(:a %) does in this case.
They should really both check the same, but I haven't fleshed out how to use the same technique for both. #(:a %) was only supported in August 2023. I refused to use the same strategy as TypeScript, which is basically choose the dynamic type for the function argument. That was one of the things I worked out during my PhD.
Looking through the tests, there are examples of polymorphic functions being passed to others just fine like (into [] (map identity) [1]). I'll see if there's something special about keywords after all.
Ah yep, this works (map (t/ann-form :a (t/All [x] ['{:a x} :-> x])) [{:a 1}])
The keyword is not being upcast property in (map :a [{:a 1}]).
Interesting do you think this is related to why that first example in this thread doesn't type check as well?
I don't think so, this is specifically about passing a keyword as a function to a polymorphic hof.
The symbolic inference doesn't even bother to fire, since it only kicks in if it literally sees a function type.
So it just needs a bit of help to realize keywords are function types.
Hmm ok it just seems all of these are relating to the use of keywords as functions and maps
I figured they may be a common thread between them
yes that's exactly what I'm saying, I must have looked at the wrong example
I'll cue up a release since it looks like there's some other good improvements waiting for release.
Oh wow awesome. Thanks I appreciate all your help, This is a big upgrade for our use case. I'm really hoping we can make this library work well for us.
1.2.1 should be on clojars
Also to overcome this issue (and (string? at) (string? rt) (string? v1) (string? v2) (string? v3) ...) I was able to write a macro that is just like every? except it expands into the above lol.
haha oh dear
Did you have to change it to get it to type check? What would you have written for untyped clojure?
You shouldn't need to coerce at and rt to strings. Does it type check without it?
Try this:
(defn get-tokens []
(let [res (prepare-execute-one!
"SELECT access_token, refresh_token FROM token_config" [])
at (:access_token res)
rt (:refresh_token res)]
(when (and at rt)
{:access_token at
:refresh_token rt})))In untyped clojure I just had
(defn get-tokens []
(prepare-execute-one!
"SELECT access_token, refresh_token FROM token_config" []))
and without the coercion it complains that access_token and refresh_token are t/Any which makes sense because the keywords can return any type rightYes. You'd probably want to throw an exception instead of returning nil right?
Possibly or I can let the caller just handle the nil case but what are you thinking if I threw an exception ?
Would that somehow not require the coercion?
yes, throwing satisfies any type
Something like:
(defn get-tokens []
(let [res (prepare-execute-one!
"SELECT access_token, refresh_token FROM token_config" [])
at (:access_token res)
rt (:refresh_token res)]
(if (and at rt)
{:access_token at
:refresh_token rt}
(throw ...))))
Then you can change the type to be non-nilable
Ok I think I like that much more, but it still seems to not be able to tell that access_token and refresh_token should be t/Str not t/Any perhaps it would be better to do something like (t/inst :access_token t/Str) to satisfy that?
hmm ok what is the output of (t/ann-form res t/Nothing) ?
I think you're right, I probably haven't implemented updating a non-heterogenous map's keys.
Oh I misread what the type of prepare-execute-one! was. You're right.
So this could work:
(defn get-tokens []
(let [res (prepare-execute-one!
"SELECT access_token, refresh_token FROM token_config" [])
at (:access_token res)
rt (:refresh_token res)]
(if (and (string? at) (string? rt))
{:access_token at
:refresh_token rt}
(throw ...))))please post the error if it doesn't
Yeah that is what I thought would work as well but it still thinks its a t/Any for both.
Type mismatch:
Expected: t/Str
Actual: t/Any
in:
at
Type mismatch:
Expected: t/Str
Actual: t/Any
in:
rtI think I know what the problem is and I'm not sure how to work around it.
at some point I broke this kind of inference by introducing a major perf improvement.
Try this:
(defn get-tokens []
(let [res (prepare-execute-one!
"SELECT access_token, refresh_token FROM token_config" [])
at (t/tc-ignore (:access_token res))
rt (t/tc-ignore (:refresh_token res))]
(if (and (string? at) (string? rt))
{:access_token at
:refresh_token rt}
(throw ...))))
Ok that makes sense because there have been other places where I would expect calling functions like int? or string? in a condition to work but instead I had to coerce the value again to whichever type to make it type check. I figured either I was doing something wrong or maybe a bug. Putting the tc-ignore allowed it to type check. Thanks.
FYI there's a flow typing feature called occurrence typing (to type different occurrences of the same local at different types based on where it is in the program).
You can also update sub-parts of a local.
So at is invisible to the type checker here, it's actually represented more like (get res :access_token).
And the tc-ignore says, just treat it as at.
I think this works well with HMap's. but a lot of other types are broken.
feel free to file a bug
Yeah thats very interesting. Because it still seems to know that :access_token in the return map must be a string. Even if I change the (string? at) to (int? at) it will fail saying :access_token in the map has to be a string. So tc-ignore is just ignoring the type at the line in the let but not later on in the function. That makes it much nicer than if it just always ignored that value for the rest of the function.
And the best place for a bug report is I assume in the issues on the repo?
yes please