Polymorphic procedures

Polymorphic procedures allow the programmer to express type-generic code, code that does not care what type is being used. This is by far the most powerful feature in Onyx.

Polymorphic procedures use polymorphic variables. A polymorphic variable is declared using a $ in front of the name. When calling a polymorphic procedure, the compiler will try to solve for all of the polymorphic variables. Then, it will construct a specialized version of the procedure with the polymorphic variables substituted with their corresponding value.

Here is an example of a polymorphic procedure that compares two elements.

min :: (x: $T, y: T) -> T {
    if x < y do return x;
    else     do return y;
}

x := min(10, 20);
y := min(40.0, 30.0);

// Errors
// z := min("Hello", "World");

$T declares T as a polymorphic variable. When min is called with two i32s, the compiler solves for T, finding it to be i32. Then a specialized version of min is constructed that operates on i32s. A very similar thing happens for the second call, except in that case T is f64. Notice that any error will occur if min is called with something that does not define the operator < for T.

Polymorphic variables can occur deeply nested in a type. The compiler employs pattern matching to solve for the polymorphic variable.

foo :: (x: &[] Iterator($T)) {
    // ...
}

val: &[] Iterator(str);
foo(val);

Here is a simple pattern matching process that the compiler goes through to determine the type of $T.

Variable TypeGiven Type
&[] Iterator($T)&[] Iterator(str)
[] Iterator($T)[] Iterator(str)
Iterator($T)Iterator(str)
$Tstr

If at any point the types do not match, an error is given.

Parameters can also be polymorphic variables. If a $ is placed in front of a parameter, it becomes a compile-time "constant". A specialized version of the procedure is made for each value given.

add_constant :: ($N: i32, v: i32) -> i32 {
    // N is a compile-time known integer here.
    // It is equivalent to writing '5'.
    return N + v;
}

println(add_constant(5, 10));

Types can be passed as constant through polymorphic variables. Consider this example.

make_cubes :: ($T: type_expr) -> [10] T {
    arr: [10] T;
    for 0 .. 10 {
        arr[it] = cast(T) (it * it * it);
    }
    return arr;
}

arr := make_cubes(f32);

Because T is a constant, it can be used in the type of arr, as well as in the return type.