Hi all,
Time for an update on the direction I hope to steer core.typed in the next year.
# Gradual typing.
We've already seen some prototyping of the core.typed REPL. This will
evolve into basically Typed Racket for Clojure, with automatic contract
enforcement for "gradually typed" namespaces.
One question is: will the core.typed that exists today be subsumed by gradual typing?
That is, will we still be able to use check-ns and completely optional type checking,
instead of being integrated into the REPL (a requirement of gradual typing).
I think the "optional" type system core.typed currently provides is very useful, and
probably a tool that should stick around.
I can see core.typed being used in several ways:
1. Annotating untyped namespaces without checking (purely documentation)
2. Optional type checking (can ship without core.typed)
3. Gradual type checking (need to AOT or ship with core.typed)
What happens if (3) interacts with (2)? Does (3) need to be protected from (2)
as if it was untyped like (1)?
Typed Clojure probably needs more knobs than Typed Racket here, while striving
for the safest defaults.
Immediate steps are:
- improving contracts support (adding higher-order casts)
- safely import untyped code and export gradually typed code via rewriting
- fix the quirks around REPL evaluation
- eg. vim-fireplace likes to evaluate its internal completion code, which doesn't type check
# Propositions and Objects
There are some things I want to do with propositions.
1. Remove negative propositions
Negative type propositions can be replaced by the Not type (which I plan to add
better support for).
(! t 0) ==> (is (Not t) 0)
2. Better syntax for propositions
The current syntax is not good, especially with larger objects.
Even `identity` can be improved.
clojure.core/identity
(All [x] [x -> x
:filters {:then (! (U nil false) 0)
:else (is (U nil false) 0)}
:object {:id 0}])
My first idea is to make :filters optional if you know the :object --- they can be trivially
inferred anyway. We can drop the :id.
clojure.core/identity
(All [x] [x -> x :object 0])
It would be even nicer if we supported named parameters instead of just De Bruijn indices.
clojure.core/identity
(All [x] [a :- x -> x :return-val a])
I also want to move away from using propositions and use
type environments at the syntax level. eg.
clojure.core/identity
(All [x] [a :- x -> x
:then [a :- (Difference x (U nil false))]
:else [a :- (U nil false)]
:object a])
That is, if `identity` returns true then `a` (ie. the argument) is of type
(Difference x (U nil false)) with some chosen `x`. This seems more readable.
I had an idea that every FilterSet can also carry a cached type environment with respect to the type environment it was created in, which
might make `filter` easier to type check with something like
clojure.core/filter
(All [x y]
[[a :- x -> Any :then [a :- y]]
(Option (Seqable x))
-> (ASeq y)])
In theory, looking up `y` is just a pattern match to the :then type environment
in eg. the annotation for identity.
# Refinement types
I want to type check code like this:
(fn [x :- (U Sym nil)]
(let [[sym?] (if (symbol? x) [true] [false])
(when sym?
(name x))))
There are various ways to do this (one is to store the current environment
in each element of a HVec) but they all seem to generalise to refinement
types.
Another is to make the TCResult triple type/propositions/object be a real type,
which ends up being a kind of refinement type. One advantage to this approach
is that we only need to change a few places for all expressions to return refinement
types.
The type of `true` --- and thus sym? --- above would be
(Refine true
:then [x :- Sym]
:else [x :- Nothing])
so we can infer [x :- Sym] in the `name` call.
# Transducers
Supporting transducers requires more than just annotating them. The local
type inference implementation needs restructuring to improve constraint
resolution.
Even (map identity coll) cannot be currently checked, let alone inferring
an appropriate polymorphic type for the transducer (map identity).
Luckily, that is exactly what
this paper tackles (see the example on the first page!).
We're pretty sure it can also check (map #(inc %) coll) for us. ie. elide annotations
for local functions.
It's unclear how far this inference goes --- I have a hunch it can help infer the
type for entire top-level functions if it knows the type of all the vars used inside of it.
I plan on chipping away at this over the next year.
# Type check more core functions
I've revisited some old favourites. Here's some notes I've compiled.
clojure.core/some-fn
; - Unions need to support dots for this to work:
; - ...+ is a non-empty ...
; - (or [y :- t] ... b) is a disjunction of propositions
; expanded by dots (no idea how to implement this) like
; (or [y :- t] [y0 :- t0] [y1 :- t1])
; - [y ... b :- e ... b] is a conjunction of propositions like
; [y :- e, y1 :- e1, y2 :- e2]
;
; (All [i t e r b ...]
; [[x :- i -> r :then [x :- t], :else [x :- e]] ...+ b
; -> [y :- i ... b -> (U r ... b)
; :then (or [y :- t] ... b)
; :else [y ... b :- e ... b]]])
clojure.core/get-in
; - I have no idea how to assign a type to `m`, even with
; refinement types.
; (All [m k ...]
; [m '(k ... k) -> (GetIn m '(k ... k)) :object {:id 0 :path [(GetIn k ... k)]}])
clojure.core/assoc-in
; (All [[m :< (Associative Any Any)] v k ...]
; [m '(k ... k) v -> (AssocIn m '(k ... k) v)])
clojure.core/split-with
(All [x y z]
[[x -> Any
:then [0 :- y],
:else [0 :- z]]
(Option (Seqable x)) -> '[(ASeq y) (ASeq z)]]
Anyway, happy hacking!
Thanks,
Ambrose