Generics
Generics allow you to write code that is independent of specific types. In Cyrus, only named types and functions can be generic. Unnamed types (like raw tuples) and anonymous functions (lambdas) do not support generic parameters.
Generic Functions
A function becomes generic by adding type parameters within angle brackets <T>.
// A simple identity function for a fixed-size array
fn identity_pair<T>(values: T[2]) T[2] {
return values;
}
pub fn main() {
// Inferred
const p1 = identity_pair({1, 2});
// Explicit type args
const p2 = identity_pair<int>({3, 4});
printf("%d %d\n", p1[0], p2[0]);
}
Recursive Generics
Generic functions in Cyrus support recursion, provided the logic allows for termination.
fn count_to_five<T>(x: T) {
printf("%d ", x);
if (x < 5) {
count_to_five(x + 1);
}
}
Generic Structs and Enums
Structs, Unions, and Enums can all take type parameters.
Structs and Type Inference (_)
You can use the underscore _ to tell the compiler to infer a specific type argument while you manually provide others.
struct Pair<K, V> {
pub key: K;
pub value: V;
}
pub fn main() {
// Explicit K (uint), but let compiler infer V
const entry = Pair<uint, _> {
key: 1,
value: "Cyrus"
};
}
Generic Enums
Generics are powerful when combined with enums for representing optional values or results.
enum Option<T> {
Some(T),
None
}
fn print_opt(opt: Option<int>) {
switch (opt) {
case .Some(val) => {
printf("Value: %d\n", val);
}
case .None => {
printf("Nothing\n");
}
}
}
Generic Methods
Even if a struct is not generic, its methods can be. If a struct is generic, its methods can introduce additional_type parameters.
struct Math {
// A generic method in a non-generic struct
pub fn add<T>(x: T, y: T) T {
return x + y;
}
}
struct Box<T> {
value: T;
// 'Self' refers to Box<T>
pub fn new(val: T) Self {
return Self { value: val };
}
}
Default Type Parameters
You can provide default types for generic parameters. If the type cannot be inferred and isn't provided, the compiler falls back to the default.
struct Result<V, E = uint64> {
pub value: V;
pub error_code: E;
}
pub fn main() {
// Uses default uint64 for E
var res = Result<char*, _> { value: "Success", error_code: 0 };
}
Generic Type Aliases
Type aliases can be used to create "shorthands" for complex generic types, or they can be generic themselves.
struct Pair<K, V> {
key: K;
value: V;
}
// Non-generic alias for a specific generic instance
type IntPair = Pair<int, int>;
// A generic alias
type Handler<T> = fn(T) void;
pub fn main() {
const log: Handler<int> = fn(x: int) void {
printf("%d", x);
};
}
Generic Interfaces
Interfaces can define generic contracts. A struct can implement a specific instantiation of a generic interface or be generic itself to satisfy it.
Struct implementing a specific version:
interface IValidator<T> {
fn validate(&const self, value: T) bool;
}
struct AgeValidator : IValidator<int> {
pub fn validate(&const self, val: int) bool {
return val >= 18;
}
}
A generic struct implementing a generic interface:
interface Shape<T> {
fn area(&const self) T;
}
struct Rectangle<T> : Shape<T> {
width: T;
height: T;
pub fn area(&const self) T {
return self->width * self->height;
}
}
Separating Type Arguments
When calling a generic method on a generic type, the type arguments are split between the type constructor and the method call.
- Type Parameters: Belong to the struct/enum/union itself and are provided after the type name.
- Method Parameters: Belong to the specific method and are provided after the method name.
struct InfixCalc<T> {
pub x: T;
pub y: T;
pub fn new(const x: T, const y: T) Self {
return Self { x, y };
}
// Generic method within a generic struct
pub fn add_to_x<V>(&self, const val: V) {
self->x += @cast(T, val);
}
// Static generic method
pub fn static_sum<K>(const a: T, const b: K) T {
return a + @cast(T, b);
}
}
pub fn main() {
var calc = InfixCalc.new(5, 7);
// Provide type arg for the method only
// If you add type arg to instance would lead to compile time error.
calc.add_to_x<uint32>(1);
calc.add_to_x<uint64>(2);
printf("%d\n", calc.x);
// For static calls, you may need to provide both:
// InfixCalc<int64> identifies the type
// static_sum<uint> identifies the method specialization
const result = InfixCalc<int64>.static_sum<uint>(10, 20);
printf("%d\n", result);
// Also you could let the compiler infer it:
const result = InfixCalc.static_sum(10, 20);
printf("%d\n", result);
}
This clear separation ensures that the compiler knows exactly which part of the generic hierarchy you are specializing at any given time.

