core-typed

Olical 2025-05-02T14:47:16.936999Z

Hey! As you can see above, I'm trying to get into Typed Clojure and experimenting with integrating it into my editor and general workflow 🙂 I'm struggling to learn how to write in Typed Clojure at the moment though. The main thing I'm struggling with is writing a function that accepts t/ExInfo (which were thrown by typed clojure!) and extracts values into t/HMaps. I think the thing I'm struggling against is the map polymorphism, I've added a bunch of heavy handed asserts trying to help it along but I'm clearly missing something. Are there any docs I should be reading for this particular area of problems?

(t/ann ExceptionInfo->type-errors [t/ExInfo :-> (t/Seqable TypeError)])
(defn ExceptionInfo->type-errors
  "Takes an ExceptionInfo from Typed Clojure and converts it to a sequence of maps we can easily display."
  [exinf]
  (let [errors (:errors (ex-data exinf))]
    (assert errors)
    (map
     (t/inst
      (fn [type-error]
        (let [{:keys [env form type-error]} (ex-data type-error)
              {:keys [line column file]} env]
          (assert (keyword? type-error))
          (assert (number? line))
          (assert (number? column))
          (assert (string? file))
          {:message (ex-message type-error)
           :form form
           :type-error type-error
           :env {:line line
                 :column column
                 :file file}}))
      [t/ExInfo :-> TypeError])
     errors)))
The best resource I've found so far is https://api.typedclojure.org/latest/typed.clj.runtime/clojure.core.typed.html - not sure if there's a nice reference that walks through all the features in one document that I've missed?

Olical 2025-05-06T08:34:25.410699Z

Ohhh this is super super useful, thank you! I like the metadata approach and respect the goal of stripping it out of the runtime. I think that's a really good thing to aim for.

Olical 2025-05-06T09:04:48.473219Z

Ah hah! It type checks! Not sure if this is "good" typed clojure code though! Would love feedback if you get a chance but no pressure 🙂

(t/defalias TypedClojureExInfoData
  (t/HMap
   :mandatory {:form t/Any
               :type-error t/Keyword
               :env (t/HMap
                     :mandatory {:line t/Num
                                 :column t/Num
                                 :file t/Str})}))

(t/defalias TypeError
  (t/U
   TypedClojureExInfoData
   (t/HMap
    :mandatory {:message (t/Nilable t/Str)})))

(t/ann ExceptionInfo->type-errors [t/ExInfo :-> (t/ASeq TypeError)])
(defn ExceptionInfo->type-errors
  "Takes an ExceptionInfo from Typed Clojure and converts it to a sequence of maps we can easily display."
  [exinf]
  (let [errors ^{::t/unsafe-cast (t/Seqable t/ExInfo)} (:errors (ex-data exinf))]
    (map
     (fn [error]
       (let [data ^{::t/unsafe-cast TypedClojureExInfoData} (ex-data error)
             {:keys [env form type-error]} data
             {:keys [line column file]} env]
         {:message (ex-message error)
          :form form
          :type-error type-error
          :env {:line line
                :column column
                :file file}}))
     errors)))

Olical 2025-05-06T09:05:05.053579Z

Being able to union HMaps makes my brain happy.

Olical 2025-05-06T09:05:42.767719Z

I do have to put the metadata on the map I'm pulling from though, not on the symbol I'm binding to, it doesn't like that.

2025-05-06T17:46:16.287269Z

Good to know, adding to the binding should definitely be supported. (probably a bug)

2025-05-06T17:49:17.409689Z

Yes unioned HMaps are a great match for how Clojure is used.

2025-05-06T17:52:26.390759Z

The code you wrote is fine. I might add a couple of cheap assertions like (assert type-error) just to compensate for the unsafe casts.

2025-05-06T17:56:31.862069Z

I think you want t/Merge instead of t/U for TypeError.

2025-05-06T17:59:42.508299Z

Right now, the :message key can probably be any type (try it). t/HMap is open by default, and the checker will pick one element of the union to check the output against. It might be worse actually, you could probably output {:message "string" :form "junk" :type-error "junk" :env "junk"} and because that's technically a '{:message t/Str} (HMap's are open by default), I think that would type check.

Olical 2025-05-06T20:16:56.203549Z

Ah understood, that makes a lot of sense. I was also wondering if using malli at runtime to read in things from the outside world or ExData would work well. So malli would runtime type check the values and (I think?) typed clojure can use those malli schemas to understand what the data looks like. Maybe that's the motivation behind the malli integration?

👍 1
Olical 2025-05-06T20:17:14.447499Z

Thank you very much for all of the advice, I'll try to spread this knowledge to others 🙂

Olical 2025-05-02T14:49:18.267859Z

I am trying to type check my type checker, it's not really required but I thought it'd be a nice dog food test and a good learning exercise for the type system. I might have to give up on this part though and just move onto to trying to get my PoC working end to end. I've lost a few hours to this already 😬 Translating between the untyped world of Map Any Any to something known seems a bit tricky. Is the canonical way to do that asserts at runtime?

Olical 2025-05-02T14:49:44.981499Z

Oh and I define TypeError as

(t/defalias TypeError
  (t/HMap
   :mandatory {:message (t/Nilable t/Str)
               :form t/Any
               :type-error t/Keyword
               :env (t/HMap
                     :mandatory {:line t/Num
                                 :column t/Num
                                 :file t/Str})}))

Olical 2025-05-02T14:53:18.867859Z

I guess a big issue is that I'm struggling to read the output like:

Cannot instantiate non-polymorphic type: (t/I [t/Nothing :-> t/Nothing :filters {:then ff, :else ff}] Fn)

...

│ Polymorphic function map could not be applied to arguments:
│ Polymorphic Variables:
│       c
│       a
│       b :..
│
│ Domains:
│       [a b :.. b :-> c] (t/NonEmptySeqable a) (t/NonEmptySeqable b) :.. b
│       [a b :.. b :-> c] (t/Seqable a) (t/Seqable b) :.. b
│
│ Arguments:
│       t/TCError t/Any
│
│ Ranges:
│       (t/NonEmptyASeq c)
│       (t/ASeq c)
│
│ with expected type:
│       (t/Seqable background-check.runner/TypeError)
Some of the notation doesn't make sense to me yet, but I'll keep reading.

Olical 2025-05-02T15:29:30.219419Z

Especially this part:

│ Arguments:
│       (t/I [t/Nothing :-> t/Nothing :filters {:then ff, :else ff}] Fn) t/Any
I can't find a reference to what t/I actually is or what this pertains to 🤔

2025-05-02T18:57:35.912559Z

t/I is an intersection type.

2025-05-02T18:58:57.437969Z

(t/doc-clj t/I) should give a little more information, and that works for lots of syntax.

2025-05-02T19:02:43.680749Z

I think I wrote a few tutorials about how to read the (admittedly bad) error messages. Here's one that seems serviceable https://stackoverflow.com/a/21682976

2025-05-02T19:04:34.051249Z

In this case, the problem is:

Arguments:
   <...> t/Any
Does not match either:
Domains:
   <...> (t/NonEmptySeqable a) <...>
   <...> (t/Seqable a)         <...>
i.e., map expects a collection as the second argument, and it's receiving t/Any.

2025-05-02T19:36:18.542689Z

There's no one golden walkthrough. Every time I sit down to do that, I get cold feet and want to change things.

2025-05-02T19:36:53.980469Z

Or the state of things is just too unusable to want to encourage people to use it yet.

Olical 2025-05-05T10:37:47.919969Z

Ohh thank you! I didn't realise that was a valid symbol at the REPL, this is a very teach a man to fish moment. Thanks! I think I tried to find that in the docs and it wasn't there and I feel like it wasn't in my autocomplete which is probably user error.

❤️ 1
Olical 2025-05-05T10:38:35.090879Z

And I get what you mean, it's all good, I'll keep pushing through and learning then try to share what I find.

Olical 2025-05-05T10:38:47.805489Z

Thanks for the tips!

Olical 2025-05-05T10:47:25.067989Z

So I guess my question now is do you have one more fishing tip around mapping unknown structures into the type system? In this case, ex-data from within an exception? It reminds me of when you try to ingest JSON into a strong type system, you need to somehow safely read it in from an unknown blob into something known at runtime. Is there a special function for this or should I rely on assert to create runtime guarantees that the type checker can lean on? Because this is a real runtime issue, right? I don't know what kind of ex-data I'm going to get out really, so I need the runtime code to robustly check that and bail early while also teaching the type checker about the data that can possibly flow through.

Olical 2025-05-05T10:47:50.980749Z

I think this'll be a SUPER common problem for me with types + Clojure, it's just the nature of the beast. So I better get good at it fast 😄

Olical 2025-05-05T11:24:29.163029Z

I feel like cast is what I want? So that it'll runtime error if it can't cast. Although maybe things like the typed let will be enough.

Olical 2025-05-05T11:27:01.403929Z

Although I've found the typed let / fn blocks cause clj-kondo to have a bad time. Maybe that's something I can configure myself out of but I might lean towards more assert based syntax for now and annotate at the function boundaries instead.

2025-05-05T23:26:51.766639Z

t/I isn't autocompletable since no Typed Clojure types or primitives are backed by vars (any more). used to work, but I removed it. It definitely has a hit on usability, but it helps towards the goal of completely removing Typed Clojure at deployment. Maybe e.g., conjure could scan for ^:typed.clojure metadata and activate t/doc-clj as a fallback for K? Or even we could just alter-var-root clojure.repl/doc to have the same logic if (-> *ns* meta :typed.clojure) . That would work seamlessly with all tooling.

👀 1
2025-05-05T23:47:28.809829Z

In terms of runtime assertions, there's clojure.core.typed/pred which generates a predicate for a type that you can use for assertions. clojure.core.typed/cast is similar but it's can wrap the output, which has performance implications. https://github.com/typedclojure/typedclojure/tree/main/typed/malli#validation: typed.malli/validate generates a malli validator based on a Typed Clojure type. I thought I implemented malli.core/validate awareness but I can't find it. You can also just https://github.com/typedclojure/typedclojure/blob/0b3ffb01b6a943bdfe0c279cfc7aae78304bec7c/typed/clj.checker/src/typed/cljc/doc.clj#L277-L294 with metadata. ^{::t/unsafe-cast T} E For debugging, the ^::t/dbg E idiom is helpful. best to wrap E in a (do) if it's a macro or not IMeta. You can stack them like ^::t/dbg ^::t/ignore ^{::t/unsafe-cast T} E which prints the resulting type, tells the type checker to not even attempt to check E, then unsafely assigns the result as type T.

2025-05-05T23:48:59.164369Z

The typed macros are basically the old style to me. You shouldn't need to t/let. Instead use:

(let [^{::t/- T} B E])

2025-05-05T23:49:27.889039Z

This works for any macro that binds variables. See the doc-clj docs about ::t/-.

2025-05-05T23:53:24.278139Z

The ideal I'm shooting for is being able to completely strip out all Typed Clojure jars at deployment. So I'm optimizing for being able to just (:require [typed.clojure :as-alias t]).

2025-05-05T23:55:05.101299Z

I have some ideas on how to reliably sweeten the syntax to (let [^{:- T} B E]) but for now it's ugly but has no runtime deps.

2025-05-05T23:56:06.381899Z

Maybe it should be the default now that we opt-in namespaces to being typed.

2025-05-05T23:58:05.320359Z

the problem is if a macro adds :- metadata, we don't want to parse it as a type unless we're completely sure it's intended as a typed op.