VocalBit« assorted thoughts on technology, computers and programming »

Exploring Clay - Type Inference and Generic Functions

A high level introduction to type inference and generic functions in the Clay programming language.

Generic Functions with Inferred Types

Clay is an upcoming programming language that fascinates a long time Python programmer like me because of its simple and powerful type inference. Consider the following code:

compute(a, b, c)
{
  return a + b + c;
}

main()
{
  println(compute(1, 2, 3)); // prints 6
}

While I don't provide any type declarations for the parameters to compute(), Clay automatically infers that they are integers based on the call to compute(1, 2, 3). That's not all - suppose I add another call to compute:

main()
{
  println(compute(1, 2, 3)); // prints 6
  println(compute(1.5, 3.8, 2.0)); // prints 7.3
}

For the second call, Clay infers the arguments as Float. The compute() function - declared without any type specifications whatsoever - is really a generic function. In the above example, Clay compiles two versions of the function - one for compute(a:Int, b:Int, c:Int) and another for compute(a:Float, b:Float, c:Float).

In fact, generics run deep in the Clay language philosphy - since '+' is defined for StringConstant, I can even do:

main()
{
  println(compute(1, 2, 3)); // prints 6
  println(compute(1.5, 3.8, 2.0)); // prints 7.3
  println(compute("a", "b", "c")); // prints abc
}

Clay does whole program analysis and type propagation - I believe it finds all calls to every function and determines all possible permutation of argument types for function calls.

Static Verification

Unlike Python and other dynamically typed languages, Clay can catch the following error at compile time:

main()
{
  compute(1, 2, "c"); // compiler emits 'error: no matching operation'
}

While doing type propagation, Clay finds that the body of compute() executes a + b + c where b is an Int, and c is a StringConstant. Because '+' is not defined between these two types, the 'no matching operation' is emitted.

Restricting Types

What if you want to ensure that compute() is only called with Int arguments? You can rewrite compute() and specify the type of each parameter as:

compute(a:Int, b:Int, c:Int)
{
  return a + b + c;
}

Now the compiler will error out if you pass in any argument that is not an Int.

Compare the generic and non-generic versions of compute(). When you rewrite a non-generic function as a generic function in C++ or Java, you have to add more noise to your code by injecting something like template <class T>.

In Clay, however, you can take a non-generic function and simply remove your type declarations to make it generic - as if to mean "I don't care what the types of the arguments are, as long as the body of this function is valid for the provided arguments". The compiler automatically makes the function generic and catches type errors.

Semi-Restricting Types

Suppose you want to write a function that applies to Int, Float and other numeric types, but not to any non-numeric types. You can write this using a guard pattern:

[T | Numeric?(T)]  // guard pattern
compute(a:T)
{
  ...
}

main()
{
  compute(1); // OK
  compute(1.2); // OK
  compute("abc"); // compiler error - 'no matching operation'
}

The guard pattern consists of a pattern variable (T), followed by a vertical bar and then a boolean expression (Numeric?(T)). You can read the vertical bar as 'where'. The guard simply means "the following is applicable for any T where Numeric?(T) is true". The pattern variable is used as the parameter's type in (a:T). Combined with the guard, this implies that this function can only accept an argument of numeric type. Note that Numeric?(Int) and Numeric?(Float) are both true but Numeric?(String) is false.

By the way, the question mark is part of the function name (in Clay identifiers can have the '?' character and by convention it is used for functions that return true or false). You can use more than one pattern variable as well as user defined functions in the guards - making the whole system quite powerful.