Developing a gradual typing system for Clojure requires a rich contracts library. Some libraries already exist like Schema and core.contracts. These do not provide more advanced contracts like polymorphic function contracts or dependent contracts. This article explores the possibilities in implementing polymorphic function contracts in Clojure.
Racket's contract library has the concept of Opaque values which can be effectively locked with a key. This is a problem when directing control flow, because the only difference we want from adding contracts is more errors and there is potential to change return values. For example
(defn minc [n]
(if (number? n)
(inc n)
n))
(mine 1)
returns 2 but if we wrap it in an opaque value it returns 1.
The solution to this issue is to disallow any predicate tests on opaque values, which is impossible on the JVM.
Instead, I propose another solution where the caller provides contracts the function uses to check itself. This is analogous to intantiating a polymorphic function type with types, but at the contract level.
Ideally, core.typed should infer and insert the contracts necessary when importing typed code. Exporting typed code is a different story, as core.typed has no control over untyped use-sites so manual instantiation is unavoidable.
Take the example of minc
defined in an untyped namespace.
(ns untyped)
(defn minc [n]
(if (number? n)
(inc n)
n))
(ns typed
{:core.typed
{:untyped-imports
{t/minc (All [a] [a -> (U Num a)])}}
(:require [clojure.core.typed :as t]
[untyped :as u]))