Generic Types

A generic class or method is one which has type parameters. A type parameter is a variable which stands for an as-yet-unknown type. For instance, the Array class can hold objects of any type. An Array<String> is not the same as an Array<Int>, but the Array class was written in such a way that it can be specialized against any type.

Generic Classes

To declare a generic class, include its type parameters after its name:

class FancyMap<Key, Value> {
    ...
}

The FancyMap class has two type parameters, Key and Value. When referring to the FancyMap type, you must specify the arguments to its type parameters, as in

def m := FancyMap<Int, String>()

Within the FancyMap class, you may use the types Key and Value:

class FancyMap<Key, Value> {
    method values():Array<Value> {
        ...
    }
}

Since you have not specified any restrictions ("bound") on Key or Value, they can become any type: the default bound is Object?. To restrict the types that may be used, specify a type bound as in:

class FancyMap<Key:Hashable, Value:Object> {
    ...
}

Now Key must be a subclass of Hashable (or implement it, if it is an interface), and Value must be an Object. Changing Value's type bound from the default Object? to Object is equivalent to saying "Value may not be null".

These types may be implicitly cast to their bounds. You may call any of Hashable's methods on Key, store Key into an Array<Object>, and so forth.

Generic Methods

Generic methods are declared in the same way as generic classes: by including the type parameters after the method name. For instance,

function process<T>(values:Array<T>)

The process method can operate on any kind of Array:

process([1, 2, 3])
process(["A", "B", "C"])

Normally, as in the examples above, you do not need to specify the type(s) of a generic method when calling it. They will be inferred based on the arguments and/or expected return type of the call. But there are sometimes reasons to specify the types. For example, in the call:

process(["A", "B", "C"])

The type will be inferred as <String>, but perhaps you intended for those single-character string literals to be treated as characters. You can force this interpretation with:

process<Char8>(["A", "B", "C"])

Self-Referential Bounds

Several classes in the Frost core library, such as Equatable, refer to themselves in their type bounds. Equatable is declared as:

interface Equatable<T:Equatable<T>> {
    ...
}

This may not immediately make sense. In order to declare an Equatable, you have to... use an Equatable?

It's easier than it might look:

class Foo : Equatable<Foo> {
    ...
}

The reason for this apparent strangeness is to ensure that Equatable is symmetric. If an equatable value foo can compare itself against another equatable value bar, then bar can compare itself against foo - most likely because they are both instances of the same class.