2.7. Type Checking

By default, Orc behaves as an untyped language. However, there is an optional static typechecker built into Orc, and an accompanying set of typed syntax and type annotations to support static typechecking. It can be enabled within the Orc Eclipse plugin, or via a command-line switch. The typechecker assures, up to the limitations of its algorithm, that no type errors will occur when the program is run. This is a useful sanity check on a program, making it easier to catch bugs early, or rule out certain causes for existing bugs. In some cases, it also speeds up program development.

A full description of the Orc typechecker is available in the Reference Manual. The reference manual section for each language feature describes how that feature interacts with the typechecker.

It is beyond the scope of this document to give a full tutorial on static type checking. However, we will briefly introduce the core concepts and describe the typical approach to using the type checker on an existing Orc program with no type information.

2.7.1. Type Inference

The Orc typechecker performs type inference, meaning that it can guess and confirm the type of many expressions without any extra information.

For example, the typechecker will happily check the following untyped program without any assistance:

val pi = 3.141592653
val radius = 7
val area = pi * radius * radius

Println("Area: " + area)

This program has type Signal, since the body expression is a Println call, which publishes a signal. The typechecker verifies that all operations in the program are type-correct.

If we had introduced a type error somewhere, the typechecker would catch it. For example, both of these programs fail to typecheck:

val pi = 3.141592653
val radius = 7
val area = "pi" * radius * radius  {- type error -}

Println("Area: " + area)

val pi = 3.141592653
val radius = 7
val area = pi * radius * radius  

Println("Area ": area)   {- type error -}

2.7.2. Adding Type Information

When we begin adding function definitions to a program, the typechecker will need more information in order to operate correctly.

A defined function must have type information for each of its arguments. We add type information using the symbol ::. This is similar to the requirements on methods in languages like Java or Scala:

{- Orc -}
def square(x :: Integer) = x * x

/* Scala */
def circleArea(x: int) = x * x

/* Java */
public int circleArea(int x) { return x * x; }

If the function is recursive, we must also give its return type:

def metronome() :: Signal = signal | Rwait(1000) >> metronome()

If the function has multiple clauses, or its arguments are complex patterns, this approach can be confusing. Instead of writing the types inline, we can write a signature, an extra declaration with the argument and return types:

def sum(List[Number]) :: Number  {-  a signature for 'sum' -}
def sum([]) = 0
def sum(h:t) = h + sum(t) 

Notice the type of the list argument, List[Number]. List is a polymorphic (or "generic") type, which we will discuss further in the next section.

2.7.3. Polymorphism

The Orc type system has polymorphic, or "generic", types, such as List. These are the types of collections or containers whose contents might be of any type, so long as the type is consistent. For example, the list [3,4,5] has type List[Integer], whereas [[true], [false], []] has type List[Boolean].

A function may also be polymorphic, if it has polymorphic arguments. This must be made explicit in the function signature. For example, here is a list append function:

def append[X](List[X], List[X]) :: List[X]  {- Note the use of a type variable, X -}
def append([], l) = l
def append(h:t, l) = h:append(t,l)

The typechecker will allow append to be used on any two lists with the same type of elements; X is the name of that type. In a program that uses append, the typechecker is actually guessing a type argument, which gets bound to X.

When the programmer writes:

val a = [1,2,3]
val b = [4,5]
append(a,b)

the typechecker fills in a type argument for append:

val a = [1,2,3]
val b = [4,5]
append[Integer](a,b)

Sometimes the typechecker can't guess this type argument on its own. For example, the Channel site takes a type argument but no value arguments, so the typechecker doesn't have enough information available to guess the type argument when it encounters the call. It must be added explicitly:

{- Fails to typecheck -}
val c = Channel()
c.put(7)

{- Typechecks -}
val c = Channel[Integer]()
c.put(7)

This information may seem redundant, since c.put(7) obviously indicates that the channel will contain Integer values, but the typechecking algorithm does not use that information. It makes up for this limitation by providing more power in other areas.

This limitation is not unusual. Java constructors for generic classes have a similar requirement:

LinkedList<Integer> l = new LinkedList<Integer>();