Typing the Numeric Tower

In the past, the creators of numerical programs had to choose between simple expression of mathematical formulas and static type checking. While the Lisp family and its dynamically typed relatives support the straightforward expression via a rich numeric tower, existing statically typed languages force programmers to pollute textbook formulas with explicit coercions or unwieldy notation. In this paper, we demonstrate how the type system of Typed Racket accommodates both a textbook programming style and expressive static checking. The type system provides a hierarchy of numeric types that can be freely mixed as well as precise specifications of sign, representation, and range information—all while supporting generic operations. In addition, the type system provides information to the compiler so that it can perform standard numeric optimizations. 1 Designing the Numeric Tower From the classic two-line factorial program to financial applications to scientific computation to graphics software, programs rely on numbers and numeric computations. Because of this spectrum of numeric applications, programmers wish to use a wide variety of numbers: the inductively defined natural numbers, fixed-width integers, floatingpoint numbers, complex numbers, etc. Supporting this variety demands careful attention to the design of programming languages that manipulate numbers. Most languages have taken one of two approaches to numbers. Many untyped languages, drawing on the tradition of Lisp and Smalltalk, provide a hierarchy of numbers whose various levels can be freely used together, known as the numeric tower. For example, the following Racket expression mixes arbitrary precision integers with inexact floating-point numbers and produces a complex result: (sqrt (/ 3.14159 ((expt 2 32)))) That is, the numeric tower supports concise expression of mathematical formulas. Other languages provide static checking of various numeric operations, ensuring that results conform to machine representations of numbers. Static checking helps programmers reason about the requirements, behavior, and performance of their programs. Some languages also provide a limited ability to combine different forms of numbers together in arithmetic operations for a small set of numeric representations. No existing typed language provides as rich a numeric hierarchy nor as many generic operations as those found in Smalltalk, Scheme, or Racket. In this paper, we describe the design of the numeric tower in Typed Racket (TobinHochstadt and Felleisen 2008), which combines expressiveness with static checking. Typed Racket is an explicitly and statically typed sister language to Racket, a mostlyfunctional language (Flatt and PLT 2010). Using Typed Racket, programmers may convert untyped Racket programs by adding explicit type declarations. In the existing type system, we can encode fine distinctions in the hierarchy of numeric types and express numerous mathematical properties of numeric operations in their types. Combining these features allows programmers to state and enforce static properties about their numeric programs while maintaining the concise mathematical expression of untyped Racket. Furthermore, we can reuse standard optimization techniques to reap the performance benefits of static typing. Three features of Typed Racket support this design. Due to true union types (Buneman and Pierce 1999) the choices of numeric types do not need to reflect the underlying runtime representation of numbers nor do they affect the representation. For example, Integer is the union of positive and negative integers, yet Racket’s run-time representation has no knowledge of this division. Due to overloading with intersection types (Coppo and Dezani-Ciancaglini 1978; Reynolds 1988) the type system supports precise specification of the behavior of numeric operations such as + without necessitating multiple implementations. Thus it can express that adding two positive integers produces a positive integer and adding a negative integer to a negative floating point number produces a negative floating point value. Due to occurrence typing (TobinHochstadt and Felleisen 2008, 2010) the type checker can “lower” the numeric types of variables based on dynamic tests including predicates and numeric comparisons. The remainder of the paper begins with a series of examples that illustrate Typed Racket’s approach to numeric computations. We then describe the encoding of the type hierarchy in section 3, the typing of numeric operations using overloading in section 4, and the use of occurrence typing to refine types in section 5. Finally, in section 6, we describe our implementation, focusing on challenges concerning usability. 2 A Rich Numeric Tower We introduce Typed Racket and its approach to numeric programming with a series of small examples. The mathematical absolute value function, | − |, takes real numbers to non-negative real numbers. As figure 1 shows, a programmer can naturally express this simple fact via types. Furthermore, the function definition itself transliterates the textbook definition of abs into the concrete syntax of a functional programming language; Typed Racket’s type system accomplishes the rest. The pythagorean function also benefits from encoding sign information in the type system. Racket’s sqrt function, like its mathematical counterpart, optionally may yield complex numbers. Programmers often write programs, however, that depend on real-valued results from sqrt. To accommodate the latter, the type of sqrt in Typed Racket maps non-negative reals to non-negative reals. Because the square of any real number is provably always non-negative and the sum of two non-negative numbers is also non-negative, the type system can validate that the length of the hypotenuse of any right triangle is non-negative. See the type of the pythagorean function in figure 1. (: abs : Real → Nonnegative-Real) (define (abs x) (if (> x 0) x (x))) (: pythagorean : Real Real → Nonnegative-Real) (define (pythagorean a b) (sqrt (+ (sqr a) (sqr b)))) (: nat->hex : Natural → (Listof Byte)) (define (nat->hex n) (cond [(= n 0) ’()] [else (cons (modulo n 16) (nat->hex (quotient n 16)))])) (: sum-vector : (Vectorof Integer) → Integer) (define (sum-vector v) (define n (vector-length v)) (let loop ([i 0] [sum 0]) (if (< i n) (loop (+ i 1) (+ sum (vector-ref v i))) sum))) (: gen-random : Float Float → Float) (define (gen-random min max) (next) (+ min (/ (* (max min) x) p))) (define p ((expt 2 31) 1)) (define A (expt 7 5)) (define x 42) ; state of the PRNG (define (next) (set! x (modulo (* A x) p))) ; xi+1 ≡ A · xi (mod p) Figure 1: Numeric programs in Typed Racket Sign properties are a special case of range properties, another common set of properties that programmers want to establish. For instance, two program fragments may need to communicate via a protocol that limits the range of encoded values. A type system that supports subtyping and overloading makes it possible to mix and match fixed-width and unbounded integers, both widely used by Racket programmers. Hence, programmers can have the mathematically correct behavior of unbounded integers as the default and may still enforce range properties when desired, without explicit coercions. In the third example of figure 1, Typed Racket’s type system guarantees that the result of (modulo n 16) fits within a byte. Arguments to nat->hex can be unbounded integers, and the range properties still hold. Similarly, in the fourth example, the type system guarantees that i is of type Index, which is bounded by the maximum length of Racket vectors. This ensures that vector index computations are performed directly using machine arithmetic instead of costly arbitrary precision operations; all index computations in sum-vector use machine integers directly. Furthermore, the results of these functions can be freely mixed with unbounded integers in subsequent computations, without introducing explicit coercions. The ability to freely mix numbers from different levels of the numeric tower in arithmetic expressions is another convenience of blackboard mathematics that is important for programmers. Again, many type systems require explicit coercions for mixed value expressions, as in Standard ML, Ocaml or Haskell, or provide a limited set of built-in implicit coercions, as in C or Java. A type system that can encode the promotion rules of arithmetic operations when used on operands of mixed types saves the programmer from having to repeatedly encode these rules in his programs in an ad-hoc manner. The last example in figure 1 presents an implementation of Lewis et al. (1969)’s multiplicative congruential pseudo-random number generator that features mixed-type arithmetic with integer and floating-point numbers. Local type inference (Pierce and Turner 2000) determines that p, A and x are of type Integer and both arguments to gen-random (min and max) are of type Float. In the boldface section, the result of the subtraction, a floating-point number, is multiplied by an integer, which results in a floating-point number. This implementation is structurally that of a textbook; the actual mathematical operations are unobscured by coercions or other artifacts. 3 Encoding the Numeric Hierarchy To encode arithmetic specifications, a type system must classify numbers. For example, if a type system is to encode specifications involving sign properties, it needs to distinguish between positive and negative numbers at the type level. To reason about exactness of results, a type system needs to encode exactness of numbers as part of their type. We express distinctions along these axes using true unions and subtyping.