Skip to content

Generics

NOTE This section is updated for 0.7.9 and later. If you use a method before 0.7.9, use generic modules instead, which offers the same functionality but less granularity. Syntax is still under discussion.

Generics allow you to create code that works with arbitrary types.

Typically an entire module section is declared using the @generic attribute:

// If the module section is generic,
// then all its declarations are as well
// Note that previous to 0.7.9, this would be written "module my_module {Type};"
module my_module @generic(Type);
// Parameterized struct
struct MyStruct
{
Type a, b;
}
// Parameterized function
fn Type square(Type t)
{
return t * t;
}

We can rewrite this with individual generic declarations (note that this is not available before 0.7.9):

module my_module;
struct MyStruct @generic(Type)
{
Type a, b;
}
fn Type square(Type t) @generic(Type)
{
return t * t;
}

Generic parameters may be types or int, bool and enum constants. In the case of types, they are written as if it was a regular type alias, e.g Type. Constant parameters are written as if they were constant aliases, e.g. MY_CONST, COUNT etc.

// TypeA, TypeB, TypeC are generic parameters.
module vector @generic(TypeA, TypeB, TypeC);

An example parameterized by a constant as well as a type:

module custom_type @generic(Type, VALUE);
struct Example
{
Type[VALUE] arr;
}

The code in a generic declaration uses the parameters as if they were types / constant aliases in the scope:

module foo_test @generic(Type1, MY_CONST);
struct Foo
{
Type1 a;
}
fn Type2 test(Type2 b, Foo* foo)
{
return foo.a + b + MY_CONST;
}

To use a generic function or type, we can either define an alias for it, or invoke it directly with its parameters:

import foo_test;
alias FooFloat = Foo {float, double};
alias test_float = foo_test::test {float, double};
...
FooFloat f;
Foo{int, double} g;
...
test_float(1.0, &f);
foo_test::test{int, double} (1.0, &g);

All generics that are defined in the same parameterized module section are instantiated together, but so are any other generics in the same module that has identical parameters:

module abc @generic(Test);
// Belongs to generic 1
fn Test test1(Test a)
{
return a + 1;
}
module efg;
// Belongs to generic 1
struct Foo @generic(Test)
{
Test a;
}
// Belongs to generic 1
fn Foo test2(Test b) @generic(Test)
{
return (Foo) { .a = b };
}
// Different parameter name, defines a new generic 2
fn Test2 test3(Test2 a) @generic(Test2)
{
return a * a;
}
fn void main()
{
// This will instantiate Foo, test2 and test1,
// but not test3
Foo{int} a;
}

Just like for macros, optional constraints may be added to improve compile errors:

<*
@require $assignable(1, TypeB) && $assignable(1, TypeC)
@require $assignable((TypeB)1, TypeA) && $assignable((TypeC)1, TypeA)
*>
module vector @generic(TypeA, TypeB, TypeC);
/* .. code .. */
alias test_function = vector::test_func {Bar, float, int};
// This would give the error
// --> Parameter(s) failed validation:
// @require "$assignable((TypeB)1, TypeA) && $assignable((TypeC)1, TypeA)" violated.

In general, contracts placed on types and identifiers will combine. However, contracts on generic functions and macros do not carry over to the aggregated generic contract:

module foo;
<* @require Test.kindof == INTEGER *>
struct Foo @generic(Test)
{
Test a;
}
<* @require Test.sizeof < 4 *>
fn Test testme(Test t) @generic(Test)
{
return t * 2;
}
fn void main()
{
// This would trigger the generic contract, placed on Foo:
// testme{float}(2.0f);
// However this is fine, since
// the function contract is not checked unless invoked:
Foo{long} x;
}

Adding methods to a generic type extends it with the method for all generic, allowing the use of the generic parameters associated with creating the type:

module foo;
struct Foo @generic(Type)
{
Type a;
}
module bar;
import foo, std::io;
fn Type Foo.add(self, Type b) => self.a + b;
fn void main()
{
Foo{int} f1 = { 3 };
Foo{double} f2 = { 3.4 };
io::printn(f1.add(5));
io::printn(f2.add(5));
}

We can also extend a particular instance, but in that case we do not access the parameterization.

module foo;
struct Foo @generic(Type) { Type a; }
module bar;
import foo, std::io;
fn int Foo{int}.add(self, int b) => self.a + b;
// The below code would print "Error: 'Type' could not be found, did you spell it right?"
// fn Type Foo{int}.sub(self, Type b) => self.a - b;
fn void main()
{
Foo{int} f1 = { 3 };
Foo{double} f2 = { 3.4 };
io::printn(f1.add(5));
// io::printn(f2.add(5)); ERROR - There is no field or method 'Foo{double}.add'
}