Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
tree: d0b080d6a7
Fetching contributors…

Cannot retrieve contributors at this time

164 lines (125 sloc) 5.517 kb

Type Checking Higher-Order Functions with core.typed

by Ambrose Bonnaire-Sergeant

Problem

Clojure strongly encourages higher-order functions, but tools for verifying their use focus on runtime verification. You want earlier feedback: at compile time.

Solution

Use core.typed to type-check higher-order functions.

To follow along with this recipe, create a file core_typed_samples.clj and start a REPL using lein-try:

$ touch core_typed_samples.clj
$ lein try org.clojure/core.typed
Note

This recipe is a little different than others because core.typed uses on-disk files to check namespaces.

To demonstrate core.typed's abilities, define a typed higher-order function hash-of?, which accepts two predicates and returns a new predicate.

Use clojure.core.typed/fn> to return an anonymous function with type annotations attached:

(ns core-typed-samples
  (:require [clojure.core.typed :refer [ann fn>] :as t]))

(ann hash-of? [[Any -> Any] [Any -> Any] -> [Any -> Any]])
(defn hash-of? [ks? vs?]
  (fn> [m :- Any]
    (when (map? m)
      (and (every? ks? (keys m))
           (every? ks? (vals m))))))

Each argument to hash-of? has type [Any -> Any]: a single argument function taking anything and returning anything.

Verifying hash-of? confirms that the preceding type annotations are correct:

user=> (require '[clojure.core.typed :as t])
user=> (t/check-ns 'core-typed-samples)
# ...
:ok

Using the clojure.core.typed/cf macro, you can type-check individual forms at the REPL (or under test). Invoking hash-of? with two predicates verifies as expected, outputting the resulting type:

user=> (require '[core-typed-samples :refer [hash-of?]])
user=> (t/cf (hash-of? number? number?))
(Fn [Any -> Any])

Passing + as a predicate, however, is a type error:

user=> (t/cf (hash-of? + number?))
Type Error (user:1:7) Type mismatch:

Expected:       (Fn [Any -> Any])

Actual:         (Fn [t/AnyInteger * -> t/AnyInteger]
                    [java.lang.Number * -> java.lang.Number])

ExceptionInfo Type Checker: Found 1 error  clojure.core/ex-info (core.clj:4327)

This is because hash-of? takes a function with an Any parameter and + takes at most a Number.

Discussion

While Clojure’s built-in pre/post conditions are useful for defining anonymous functions that fail fast, these checks only provide feedback at runtime. Why not type-check our higher-order functions as well? core.typed's type-checking abilities aren’t limited to only data types—it can also type-check functions as types themselves.

By writing returning anonymous functions created with the clojure.core.typed/fn> form instead of fn, it is possible to annotate function objects with core.typed's rich type-checking system. When defining functions with fn>, annotate types to its arguments with the :- operator. For example, (t/fn> [m :- Map] ...) would indicate an anonymous function that accepted a Map as its sole argument.

Beyond definition, it can also be useful to check the types of forms at the REPL. The clojure.core.typed/cf macro is a versatile REPL-oriented tool for on-demand type checking. It proves useful not only for checking your code, but also for investigating built-in functions. Invoking cf on any of Clojure’s higher-order functions reveals their type signatures:

user=> (t/cf iterate)
(All [x]
  (Fn [(Fn [x -> x]) x -> (clojure.lang.LazySeq x)]))

The All around iterate's type indicates that it is polymorphic in x. It reads, "for all types x, takes a function that accepts an x and returns an x, and takes an x, and returns a lazy sequence of x."

The cf macro can also detect when the wrong number of arguments are being passed to a function returned by another function:

user=> (t/cf (fn [] ((hash-of? + number?))))
Type Error (user:1:15) Type mismatch:

Expected:       (Fn [Any -> Any])

Actual:         (Fn [t/AnyInteger * -> t/AnyInteger]
                    [java.lang.Number * -> java.lang.Number])
in: ((core-typed-samples/hash-of? clojure.core/+ clojure.core/number?))

Type Error (user:1:14) Wrong number of arguments, expected 1 fixed
parameters, and got 0 for function [Any -> Any] and arguments []
in: ((core-typed-samples/hash-of? clojure.core/+ clojure.core/number?))

ExceptionInfo Type Checker: Found 2 errors  clojure.core/ex-info (core.clj:4327)
Note

In this experiment, the faulty invocation of hash-of? is wrapped in an anonymous function. At the time of this writing, core.typed evaluates code before it type-checks it.

Without this, the raw invocation ((hash-of? + number?)) would return a regular Clojure ArityException.

See Also

Jump to Line
Something went wrong with that request. Please try again.