Jeremy Siek

What is Gradual Typing

Japanese translation

Gradual typing is a type system I developed with Walid Taha in 2006 that allows parts of a program to be dynamically typed and other parts to be statically typed. The programmer controls which parts are which by either leaving out type annotations or by adding them in. This article gives a gentle introduction to gradual typing.

The following were our motivations for developing gradual typing:

Before describing gradual typing, let’s review dynamic and static type checking. Dynamic type checking

A number of popular languages, especially scripting languages, are dynamically typed. Examples include Perl, Python, Javascript, Ruby, and PHP. In general, a type is something that describes a set of values that have a bunch of operations in common. For example, the type int describes the set of (usually 32 bit) numbers that support operations like addition, subtraction, etc. A type error is the application of an operation to a value of the wrong type. For example, applying concatenation to an integer would be a type error in a language where concatenation is an operation only on strings. Another example of a type error is invoking a method on an object that doesn’t implement the method, such as car.fly(). (Isn’t it a shame that flying cars have not yet hit the mainstream, and it’s well past the year 2000!) The precise definition of type error is programming language dependent. For example, one language might choose to allow concatenation of integers and another language not. In a dynamically typed language, type checking is performed during program execution, immediately before the application of each operation, to make sure that the operand type is suitable for the operation.

The following is an example Python program that results in a type error.

def add1(x):
    return x + 1

class A(object):
    pass

a = A()
add1(a)

The output from running the above program on the standard Python interpreter is

TypeError: unsupported operand type(s) for +: 'A' and ‘int'

Static type checking

There are also a number of popular statically checked languages, such as Java, C#, C and C++. In a statically checked language, some or even all type errors are caught by a type checker prior to running the program. The type checker is usually part of the compiler and is automatically run during compilation.

Here’s the above example adapted to Java.

class A {
    int add1(int x) {
      return x + 1;
    }
    public static void main(String args[]) {
      A a = new A();
      add1(a);
    }
}

When you compile this class, the Java compiler prints out the following message.

A.java:9: add1(int) in A cannot be applied to (A)
        add1(a);
        ^
1 error

You may wonder, how can a type checker predict that a type error will occur when a particular program is run? The answer is that it can’t. It is impossible to build a type checker that can predict in general which programs will result in type errors and which will not. (This is equivalent to the well-known halting problem.) Instead, all type checkers make a conservative approximation of what will happen during execution and give error messages for anything that might cause a type error. For example, the Java compiler rejects the following program even though it would not actually result in a type error.

class A {
    int add1(int x) {
    return x + 1;
    }
    public static void main(String args[]) {
    A a = new A();
        if (false)
        add1(a);
        else
            System.out.println("Hello World!");
    }
}

The Java type checker does not try to figure out which branch of an if statement will be taken at runtime. Instead it conservatively assumes that either branch could be taken and therefore checks both branches.

Comparing dynamic and static type checking

There is a religious war between people who think dynamic checking is better and people who think static type checking is better. I believe that one of the reasons why this war has gone on for so long is that both groups have good points. (They also have some not-so-good points.) Unfortunately the two groups typically don’t acknowledge the good points made by the other group as being good points. My evaluation of the points, given below, will probably annoy both the static typing fans and the dynamic typing fans. There are of course arguments to be made for or against each of the points, and the evaluation below shows where I land after considering the arguments.

Because neither static or dynamic type checking is universally better than the other, it makes sense to provide the programmer a choice, without forcing them to switch programming languages. This brings us to gradual typing.

Gradual type checking

A gradual type checker is a type checker that checks, at compile-time, for type errors in some parts of a program, but not others, as directed by which parts of the program have been annotated with types. For example, our prototype gradual type checker for Python does not give an error for the above program, reproduced again below.

def add1(x):
    return x + 1

class A:
    pass

a = A()
add1(a)

However, if the programmer adds a type annotation for the parameter x, as follows, then the type checker signals an error because the type of variable a is A, which is inconsistent with the type of parameter x of the add1 function, which is int.

def add1(x : int):
    return x + 1

class A:
    pass

a = A()
add1(a)

(Our rules for assigning static types to local variables such as a are somewhat complicated because Python does not have local variable declarations but in most cases we give the variable the same type as the expression on the right-hand side of the assignment.)

The gradual type checker deals with unannotated variables by giving them the unknown type (also called the dynamic type in the literature), which we abbreviate as ? and by allowing implicit conversions from any type to ? and also from ? to any other type. For simplicity, suppose the + operator expects its arguments to be integers. The following version of add1 is accepted by the gradual type checker because we allow an implicit conversion from ? (the type of x) to int (the type expected by +).

def add1(x):
    return x + 1

Allowing the implicit converson from ? to int is unsafe, and is what gives gradual typing the flavor of dynamic typing. Just as with dynamic typing, the argument bound to x will be checked at run-time to make sure it is an integer before the addition is performed.

As mentioned above, the gradual type checker also allows implicit conversions from any type to type ?. For example, the gradual type checker accepts the following call to add1 because it allows the implicit conversion from int (the type of 3) to ? (the implied type annotation for parameter x).

add1(3)

The gradual type checker also allows implicit conversions between more complicated types. For example, in the following program we have a conversion between different tuple types, from ? * int to int * int.

def g(p : int * int):
  return p[0]

def f(x, y : int):
  p = (x,y)
  g(p)

In general, the gradual type checker allows an implicit conversion between two types if they are consistent with each other. We use the shorthand S ~ T to express that type S is consistent with type T. Here are some of the rules that define when two types are consistent:

We write S !~ T when S is not consistent with T.

So, for example

int ~ int
int !~ bool
? ~ int
bool ~ ?
int * int ~ ?
fun(?,?) ~ fun(int,int)
? ~ fun(int,int)
int * int !~ ? * bool

Why subtyping alone does not work

Gradual typing allows an implicit cast from any type to ?, similar to object-oriented type systems where Object is the top of the subtype lattice. However, gradual typing differs in that it also allows implicit casts from ? to any other type. This is the distinguishing feature of gradual typing and is what gives it the flavor of dynamic typing. Previous attempts at mixing static and dynamic typing, such as Thatte’s Quasi-static Typing, tried to use subtyping but had to deal with the following problem. If the dynamic type is treated as both the top and the bottom of the subtype lattice (allowing both implicit up-casts and down-casts), then the lattice collapses to one point because subtyping is transitive. In other words, every type is a subtype of every other type and the type system no longer rejects any program, even ones with obvious type errors.

Consider the following program.

def add1(x : int) -> int:
   return x + 1
add1(true)

Using true as an argument to the function add1 is an obvious type error but we have bool <: ? and ? <: int, so bool <: int. Thus the subtype-based type system would accept this program. Thatte partially addressed this problem by adding a post-pass, called plausibility checking, after the type checker but this still did not result in a system that catches all type errors within fully annotated code, as pointed out by Oliart. I won’t go into the details of Thatte’s plausibility checking, as it is rather complicated, but I will discuss an example. The following program has an obvious static type error which is not detected by plausibility checking.

def inc(x: number):
   return x + 1

inc(True)

In the application of inc to True, both inc and True can be implicitly up-cast to the dynamic type ?. Then inc is implicitly down-cast to ? -> ?. The plausibility checker looks for a greatest lower bound of number -> number and ? -> ?, which is ? -> number, so it lets this program pass without warning.