Fork me on GitHub
#core-typed
<
2022-03-28
>
emccue00:03:51

(ns tyt
  (:require 
    [typed.clojure :as t]
    [malli.core :as m]))

(m/=> foo [:=> [:cat :int] :int])

(defn foo [t] 
  "abc")

(comment
  (t/check-ns-clj))

emccue00:03:24

Going through the malli example, first time in a long time trying to use typed clojure. Can't seem to get it to break. Am I doing something wrong?

emccue00:03:59

(or rather, it doesn't fail unless i restart the repl and vice versa when changing the return from "ABC" to 3)

ambrosebs00:03:53

Hmm, so check-ns-clj fails with (defn foo [t] "abc"), but only after restarting the REPL?

ambrosebs00:03:55

Oh, save the file?

emccue00:03:15

mashing cmd + S does seem to make it work. Intellij saves when you switch tabs or give it that hint

emccue00:03:24

but okay, that makes sense

emccue00:03:06

so it type checks the file on disk, not the state in the repl

ambrosebs00:03:31

exactly. Did you eval the buffer instead of saving to disk?

emccue00:03:06

yes i did. usually with IntelliJ I am not actively thinking about when the files are saved because they "just are"

👍 1
emccue00:03:50

but is it using the live repl state to do the type checking?

ambrosebs00:03:38

Type collection is live. eg., t/ann and m/=> is gathered from the REPL.

emccue00:03:44

So is this some form of caching?

emccue01:03:35

(how does it know the type when i haven't loaded it in?)

ambrosebs01:03:23

by loading the namespace in the REPL, you've evaluated m/=>. so t/check-ns-clj just retrieves it from malli.

ambrosebs01:03:43

Oh, now I get it.

ambrosebs01:03:34

Type checking works much like evaluation. except it's read -> macroexpand/type check -> eval for each form, left to right.

ambrosebs01:03:55

probably if m/=> occurred after defn it would fail

emccue01:03:23

but in this case I haven't loaded the m/=> line

ambrosebs01:03:45

Here's what happens: 1. you create the tyt namespace. 2. you call t/check-ns-clj 3. now, the type checker basically calls (require 'tyt :reload), except just before evaluating each form, the form is type checked.

ambrosebs01:03:44

so (check-form '(m/=> ..)) is called before (check-form '(defn foo ..)), so the malli schema is registered just in time.

emccue01:03:26

ooooh okay

ambrosebs01:03:51

does it still work if you switch the forms?

emccue01:03:27

it does not fail with the error if i switch the forms

emccue01:03:59

sideways question: Does this mean that typedclojure couldn't be embedded in cljkondo?

ambrosebs01:03:04

ah, so what's happening there is that if there's no expected type available, it will check as [Any -> Any] . and it succeeds.

emccue01:03:28

(or be used in a similar way)

ambrosebs01:03:01

clj-kondo is very different, and I don't know if it's possible to combine the two. Typed Clojure relies on being able to really macroexpand your code, which is exactly what clj-kondo cannot.

ambrosebs01:03:26

it's more like eastwood.

ambrosebs01:03:12

Interestingly, clj-kondo and Typed Clojure are converging in other ways, since we're both making extensible rules for macros that we don't recognize. So maybe, Typed Clojure will know so many macros eventually that we can think of a graalvm offline version. But, I still don't really understand why you'd want that over importing a jar.

ambrosebs01:03:21

The flexibility you get is perfectly demonstrated by the malli integration you're playing with. You'd need extra steps to export the malli to a clj-kondo config to do this.

emccue01:03:50

there is already that for malli

emccue01:03:02

and it goes to the "clj-kondo type declaration" format

ambrosebs01:03:27

yes, that's what made me realize the pros/cons of each approach actually.

emccue01:03:09

most of the motivation is (i think) around the editor experience, since requiring a running repl for feedback has been a barrier

emccue01:03:25

I think the folks that make better use of it could make a more convincing pitch though

emccue01:03:00

so "hooks" and the pseudo macro-expansion they provide wouldn't be enough for typed clojure?

ambrosebs01:03:09

IMO clj-kondo is perfect the way it is, I'm just commenting on what you'd lose by making Typed Clojure move to graalvm.

ambrosebs01:03:54

Theoretically it's enough.

ambrosebs01:03:20

But there are so few rules atm that we have to wait before seriously trying.

👍 1
ambrosebs01:03:17

this is the no.1 question I get in the last few years 🙂 but Typed Clojure is 11 years old at this point, it's a totally different ball game on graalvm.

ambrosebs01:03:46

The best I could hope to contribute realistically is maybe help borkdude port some of Typed Clojure's type system features to clj-kondo.

ambrosebs01:03:00

at least in the short term.

ambrosebs01:03:50

It's about the same complexity as porting eastwood to graalvm. I haven't seen that suggested anywhere, which makes me think there are some misconceptions about Typed Clojure floating around 🙂

emccue00:03:17

(also I am getting the feeling that i'm maybe a few months early since :map isn't supported)

ambrosebs00:03:44

Yes, perhaps, but you trying it out gives me an incentive to finish it so thank you. 🙂

ambrosebs01:03:08

I bet I can knock out a good chunk of support by just copying the malli.clj-kondo namespace.

ambrosebs01:03:46

I would really like to know what the real challenges are in practice (you know, asides from :map not being implemented xD).

ambrosebs01:03:29

I have a feeling we'll need a way to embed malli syntax into Typed Clojure syntax. Say you have a big map defined in malli, and you need to override a type annotation, you'd want to be able to refer to the big malli map without rewriting the whole thing in typed clojure

ambrosebs01:03:15

(def MyMassiveMalliType [:cat ... ...])
(t/ann foo [(Malli MyMassiveMalliType) -> ...])

ambrosebs02:03:25

@emccue I added a few dozen more malli -> type conversions, typed.malli 1.0.24 is currently releasing please check it out when the build finishes.

emccue02:03:09

Basic :map works

emccue02:03:47

is there a way to see what type was inferred based on a schema?

ambrosebs02:03:02

yeah, but it uses the old namespace. Try (clojure.core.typed/cf foo)

ambrosebs02:03:54

should be implicitly loaded by typed.clojure

emccue02:03:06

is check-ns-clj what i should run to type check an entire project?

emccue02:03:21

and does type checking extend into local/root dependencies?

emccue02:03:47

(I'm gonna take the plunge and try to type check the monorepo)

ambrosebs02:03:46

check-ns-clj sources files in the same way as require. It just needs to be on the classpath in the normal place.

emccue02:03:08

okay that went pretty quick - it skips checking namespaces that don't depend on clojure.core.typed

emccue02:03:57

so i guess we would add a dummy require to turn it on ns by ns

(->> (all-ns)
     (filter #(string/starts-with? (str %) "lumanu"))
     (map (comp symbol str))
     (t/check-ns-clj))

ambrosebs02:03:17

Yeah that part is in flux. Before, that was important because check-ns checked all dependencies transitively. This is no longer the case, but the requirement is still there.

ambrosebs02:03:54

I was about to lift that requirement actually.

ambrosebs03:03:57

Yeah, the workflow is still not obvious to me after all these years. I just do

(deftest type-checking
  (is (check-ns-clj '[ns1 ns2 n3])))

ambrosebs03:03:33

I'm really leaning into a single file being the unit of type checking these days. Not a project, or a set of ns dependencies. Mainly for performance reasons.

ambrosebs03:03:44

One of the reasons users stopped using core.typed was that it forced you to check dependencies before doing anything else. Which was an agonizingly long time. This is no longer the case.

emccue03:03:47

(ns ^:typecheck something.name)
Feels like the approach i'd go with
(deftest type-check
  (is (->> (all-ns)
           (filter #(string/starts-with? (str %) "lumanu"))
           (filter #(:typecheck (meta %)))
           (t/check-ns-clj)))
Slap that on CI

ambrosebs03:03:52

alright I gotta go. thanks @emccue, lmk how your experiments go!