Type Annotations
Last updated at 2005/10/09 20:45:06 PDT by Temporal

Annotating types

When you declare a variable, you may annotate its type (that is, tell the compiler what type of values the variable may store) as follows:

i: Integer

Here we declare that "i" will only contain integers. This syntax may be used anywhere where you are declaring a variable name, such as on the left side of an equals sign in a "where" block, or when declaring parameters to a function:

(x: Integer) => y where
   y: Integer = x * x

Unlike C or Java, Evlan does not require you to explicitly declare the type of variables. However, unlike many popular functional languages, it does not attempt to "infer" types either. For variables which are declared equal to some expression (e.g. in a "where" block), Evlan deduces the type of the variable based on the output type of the expression. For example, imagine that the type of "y" had not been annotated in the above example:

(x: Integer) => y where
   y = x * x

Here, the compiler would automatically assign the type "Integer" to "y" because it knows that the result of "x * x" must be an integer. Note, though, that several other bits of information could also be attached to y's type in this case. For instance, the compiler could deduce that y is non-negative.

It gets a little trickier if we remove the annotation on a function parameter:

(x) => y where
   y = x * x

Here, the compiler cannot determine the types of "x" or "y". You might think that it could assume x is a scalar since multiplication is a function of scalars, but there is a problem with this: if you called the above function on an integer, you would probably expect an integer result. And if you called it with a number in the range [4, 8), you'd expect a result in the range [16, 64). In fact, there are an infinite number of possible input types and corresponding output types for this function. Since no types were explicitly annotated, the compiler is obligated to insure that the function satisfies all of these types.

To allow all possible uses of the function to work, the compiler must type-check it in every context in which it is used. If you call it with an integer parameter, it will plug this in and determine that the result is an integer as well. If you call it with a complex number, it will determine the result to be a complex number. It may also determine additional facts, such as that the result is non-negative (unless the input was complex, of course).

If you think that this sounds incredibly inefficient, you're probably right. For programs lacking type annotations, compile times can increase exponentially with the size of the program. However, this is not an entirely bad thing, because the larger a program is, the more the program really should be using explicit type annotations anyway. Evlan's system conveniently allows programmers to write prototype code without types initially, but forces them to go back and add annotations later on. Also note that, when compile times do get too long, you can usually reduce them exponentially by adding just a couple annotations in the right places.

Types of types

When annotating the type of a variable which itself stores a type, the <: operator is used:

Element <: Comparable

This says that "Element" is a variable representing a type, and that type is a subclass of the type "Comparable". You might see a declaration like this used with a container class:

makeBinaryTree = (Element <: Comparable) => #...

You could then call this function to construct a binary tree of integers:


Why Annotate?

Type annotations are never required in Evlan. If you were to strip all the type annotations from an Evlan program, it would still run exactly as it always had. In fact, the compiler would even continue to catch many type errors in the program's source code. However, there are several very good reasons why you should not do this.

First and foremost, type annotations make your code more readable. Knowing what types you are expecting for the parameters of a function is invaluable to anyone trying to understand your code.

Second, it allows you to insure that users of your code are not relying on assumptions which you don't intend to uphold. For example, say you declared a constant like:

i = 2

The type of i is now literally the value 2. That is, i's type has only one possible value: the number 2. i can then be used anywhere where the number 2 can be used, such as in a context requiring a number between 0 and 10. Now, say later on you want to change this constant to:

i = 24

Now that code which relied on i being between 0 and 10 will fail to compile! To avoid this problem, you should have explicitly annotated i's type in the first place:

i: Integer = 2

With this annotation, the only assumption which code using i is allowed to make is that i is an integer. If some code tries to use i in a context requiring a number between 0 and 10, the compiler will produce an error, even though the actual value of i is in this range.

Finally, type annotations make the compiler's job easier. When no annotations are supplied, the compiler has to do a lot more analysis to check types. Many optimizations are available to make this faster, but in some cases the compiler may literally have to type check a function once for every time it is called. If the function is large and is called a lot, this could get very costly. Fortunately, the compiler can easily tell you what functions are consuming a lot of resources, and usually all you have to do to solve the problem is add annotations for the function's inputs and outputs.

So where should you use annotations? My recommendations:

  • Fully annotate all public interfaces. Period.

  • Annotate input types for any function longer than one or two lines.

  • Annotate input and result types for named functions.

  • Annotate local variables any time their types are not obvious.

  • Don't bother annotating code which you are typing interactively, unless you are actually trying to debug type errors.

  • Small scripts and throw-away code can probably get away without annotation.

  • When in doubt, annotate. It doesn't take long, and it often helps you understand your own code better.

evlan.org © Copyright 2003-2005 Kenton Varda
Powered by Io Community Manager, Evlan, and FreeBSD