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.

    1. Type Parameters: Belong to the struct/enum/union itself and are provided after the type name.
    2. 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.