Generics provide a method for creating functions and composite types that work across a wide range (read: all other) of types.
An example of a type definition using generics is the Option
type:
type Option<T> enum {
None,
Some(T)
};
The definition tells us that the type Option
takes a type parameter T
, which after declaration can be used as if it was just a plain type. To declare an instance of the type you can explicitly pass a type parameter, or in most cases, let the compiler infer the type for you:
a: int = 0;
x := Option::None<int>()
y := Option::Some(a);
The other use case for generics is for defining functions that act on many types. An example of this could be a type-agnostic allocation function:
func alloc<T>(): ^T {
ptr := C::malloc(sizeof(T));
... // Check allocation success
return ^T(ptr);
}
As with the Option
type the definition tells us that alloc
takes a type parameter T
. The names of type parameters have the same limits as any other identifier, so T
could've been named TypeToAllocate
and the example would've worked just as well. Invoking the functions follow the same semantics as with using a generic type. It can either be done explicitly, or by letting the compiler infer the type:
x := alloc<SomeLargeStruct>();
y: ^SomeLargeStruct = alloc();
When using generics you are not limited to one type parameter. This allows for even more complicated, but often even more useful, constructs. An example of using more than one type parameter could be a map:
type Map<Key, Value> struct {
...
};
The example works exactly like you might expect. Both type parameters can be used as if they were types within the body of the struct.
Continuing from the previous example you might notice something missing. How are you supposed to actually use the structure without copy-pasting code everywhere? Usually you would implement a method on the structure, but would that work here? Luckily methods can be declared on generic types aswell with almost no syntactical overhead. An example of defining a method an our Map
is as follows:
func (Map<Key, Value>) Get(key: Key): Value {
...
}
When defining a method on a generic type, the type parameters given to the type carry over to the function that's being implemented. Thus the Get
function has knowledge of Map
s two type parameters, Key
and Value
, and can use them just like any other type. Note that the names do not have to match with the names used to define the type, so Map<Key, Value>
might as well have been written Map<K, V>
if the author felt that more fitting.
It is not always the case that a generic function/type wishes to work with all types. To allow for the creation of functions/types like this we use the concept of type restrictions. A type restriction is, as the name suggests, a way to lock down the types that can be used with a generic type/function. To ease explaining we start with an example:
type Vector3<T: Number> struct {
x: T, y: T, z: T
};
Again we have a type with a type parameter T
, this time the parameter is followed by a colon and an identifier. This identifier, in our case Number
, is the name of an interface. It means that, T
must implement the interface Number
to be usable in a Vector3
.
Just like you can use multiple type parameters, you can also use multiple type restrictions. An example of this is the following:
type HashMapWithIteratorKeysForSomeReason<Key: Hash & Iterator, Value> struct {
...
};
This means that Key
has to both interfaces, Hash
and Iterator
. Again, the only limit to how many restrictions you can use is your imagination. And common sense.
This is great