Imagine the universe of all possible programs P. This includes syntactically incorrect programs, programs your compiler will not accept, etc. Within this universe there is some set of programs S ("Safe programs") that will never execute a type-incorrect operation, like adding a number to a string. See Fig. 1.
A static type system is a constraint upon the universe of programs a programmer may write in some language. In his textbook Types and Programming Languages, researcher Benjamin Pierce defines a static type system as follows:
Definition 1: [A static type system is a] tractable syntactic method for proving the absence of certain program behaviors by classifying phrases according to the kinds of values they compute.
One desideratum of a good static type system is that it should prove the absence of type-incorrect operations --- i.e., it should reject programs outside of S. We say such a type system is sound:
Definition 2: A type system is sound if it accepts no programs that perform type-incorrect operations.
It would be nice if we could also devise a type system that would reject only those programs outside of S. Unfortunately, it's not possible to do this in finite time:
Theorem 1: It is undecidable in the general case to determine exactly whether a program will ever execute an unsafe operation. (There is a trivial reduction from the Halting Problem.)
This is why Fig. 1 depicts set S with a dotted line: it is not possible to compute in finite time whether any given point in the space belongs to S. Therefore, Theorem 1 has the following corollary:
Corollary to Theorem 1: All sound, decidable static type systems reject some safe programs.
Fig. 2 shows the space of P further subdivided, with the set T of well-typed programs a strict subset of S. This is an important point: every static type system forbids the programmer from writing some "good" programs.
The inompleteness of static type systems is one of the arguments for dynamically typed languages:
Definition 3: A dynamically typed language is one in which type-incorrect operations are caught during program execution, rather than during a type checking phase that precedes execution.
In a dynamically typed language, every program in S can be written; indeed, every program in P can be written, and those not in S will simply raise an error at runtime, when the ill-typed operation occurs.
But there's the rub, or at least so say static typing advocates: every program in P can be written, including all the "bad" ones. In a dynamically typed language, you may not realize that your program is unsafe (i.e., outside of S) until you've run it. A long-running program may fail with a dynamic type error after executing for a week. This may be unacceptable --- you may wish to guarantee that your program will never "fall over" due to a type error.
A static type system therefore provides early feedback about possible bugs in the program --- you get to know at compile time whether your program is attempting to execute a type-incorrect operation. If the type system prevents you from writing some programs that are correct, but that the type checker cannot prove correct --- well, that's the cost of safety. Since it is well-known in the software engineering literature that bugs detected earlier are cheaper to fix, the cost in expressiveness is a price worth paying.
Static typing advocates might also say that programs that would not typecheck are probably confusingly written anyway. For example, a conditional expression in Scheme can return different types on the two branches:
(if (p) 3 "hello")
It is possible that the program is correct in this conditional
statement --- that it will always expect a number when
p
is true, and it will always expect a string
otherwise. Static typing advocates would claim that it is
nevertheless strange to write such a program, and that the
programmer is likely to forget to update this code if
p
changes. Therefore, it is better to force the
programmer to wrap up the value in, say, an ML datatype:
datatype IntOrString = Integer of int | String of string ... if p then Integer 3 else String s
Then, when treating the result of this expression, the programmer will be forced to use type-safe pattern-matching to extract the result.
The arguments for static and dynamic typing are summarized in the following table.
For static typing: | For dynamic typing: |
|
|
Notice that some of the claims of static typing and dynamic typing advocates overlap --- for example, both claim that their methods are superior for supporting rapid program evolution.
Somewhere between traditional static typing and dynamic typing lies soft typing, which is the use of a static type system as an advisory addition to a dynamically typed language.
In a soft typing system, the programmer writes his/her program as if in a dynamically typed language. Then, the soft type checker attempts to infer types for the program, and to detect type errors --- but when a static type error is detected, the programmer can choose whether to fix it, or to simply run the program anyway. In the soft typing model, type errors are warnings, not hard errors.
At first glance, this model has the best of both worlds: you get many of the software engineering benefits of static type systems, and all the flexibility of a dynamically typed language.
However, once you get into the details, it becomes quite hard to design a successful soft type system, because programmers in dynamically typed languages tend to write programs that defeat type inference. The design of successful soft type systems is an open research problem. Thus far, soft type systems have therefore mostly been the province of research projects within the Scheme community. On the PLT Scheme mailing list, in August 2001, researcher Shriram Krishnamurthi had this to say on the subject:
Every few years, yet another scripting language's community seems to discover soft typing. Maybe because I'm the PLT loudmouth, they inevitably end up in a long thread with me. I've been through this with Guile, Python and Tcl. Every time, some smart, bright-eyed and bushy-tailed person sets out to build a soft typer for their language. Every time, a few weeks in, they finally realize why this is such a hideously difficult undertaking, and give up.
In practice, the relative merits of statically and dynamically typed languages depend on an engineering question: can the static type system be designed to accept enough good programs to make it worth using?
The question of static type system engineering can perhaps be made clearer by discussing a few examples.
Trivial type system 1: All expressions have type Bad. Every operation on Bad is a type error.
This type system is sound (it accepts no unsafe programs), but it is also useless. Only the empty program can be written in this type system.
Trivial type system 2: There is one static type, Bits. All operations can be applied to Bits and return type Bits.
This type system is also sound (there is no type error ---
everything produces Bits
) --- but it is also useless,
for the opposite reason that our first type system was useless.
This type system is too permissive: it doesn't capture any of our
intuitions about "incorrect" operations as type errors.
Principle: Good type systems must balance permissiveness with strictness.
Type systems must be permissive enough to allow useful programs to be written, and strict enough to catch useful errors. (One important aspect of permissiveness that we've already seen is polymorphism, which allows code to be written for an entire family of types, rather than for one type only.)
Principle: Even sound static type systems compromise on some "type-like" errors and check them dynamically.
Consider the hd
function in ML. The type of this
function is 'a list -> 'a
. However, clearly, when
applied to the nil
list, which is a well-typed
application, hd
cannot return a useful value. One
could imagine some type system in which lists are further
subdivided into empty and non-empty static types. ML does not
take this approach. Instead, it checks dynamically and raises an
exception.
In practice, in order to be useful, all statically typed languages compromise and define some "type-like" errors as dynamic errors. The other classic example is an array-out-of-bounds error: one can imagine a language in which all arrays have statically known size. In such a language there would be no such thing as a generic array of integers; instead, there would be an array of integers of length 1, an array of integers of length 2, etc. One would then be able to check statically that all array accesses were in bounds.
The drawback of such a system is that it would not be possible to write functions over arrays of unknown size. This is not considered acceptable for most practical programming. Indeed, the original version of the Pascal language had array types that fixed the size --- it was not possible to write routines that were polymorphic over array size --- and this was one of the reasons that programmers rejected this language.
In practice, most type-safe languages allow array size polymorphism, and check array bounds dynamically.
Bob Harper, one of the designers of Standard ML, likes to say that "a dynamically typed language is a statically typed language with only one static type."
Indeed, it is easy to translate Scheme into ML by representing each Scheme value with an instance of the following type:
datatype SchemeVal = Null | Number of ... | String of string | Symbol of string | Pair of SchemeVal * SchemeVal | ...
Viewed this way, one might argue that the expressiveness argument of dynamic typing advocates is actually hollow --- if every Scheme program can be so trivially written in ML, then how can Scheme really be more expressive than ML? As a practical matter, however, using the above datatype is far too cumbersome for real programming --- one would forever be pattern matching on one or two of the cases and raising errors for the rest.
The real difference, then, between a dynamically and statically typed language is that the former makes the use of such a value convenient, by making it the default and providing easy syntax to use it.