Generics
Overview
Section titled “Overview”Generics allow writing code that works with multiple types while maintaining type safety. Ard uses a simple syntax where function generics are inferred from $T usage, and structs may explicitly declare receiver-level generics when needed.
Generic Syntax
Section titled “Generic Syntax”Generic types begin with $ in function and struct declarations:
fn apply(value: $A, transform: fn($A) $B) $B { transform(value)}In this example, $A and $B are generic type parameters. The function accepts a value of type $A and returns the $B produced by the callback.
Type Inference
Section titled “Type Inference”The compiler attempts to infer generic types from usage:
fn identity(value: $T) $T { value}
let number = identity(42) // $T inferred as Intlet text = identity("hello") // $T inferred as Strlet flag = identity(true) // $T inferred as BoolReference identity is preserved during generic inference. If $T is inferred from a mut User, the result is also mut User:
struct User { name: Str }
let user = User{name: "Ada"}let reference = mut userlet alias = identity(reference) // $T is mut Useralias.name = "Grace"A generic destination explicitly fixed to ordinary User does not silently copy the referent. Use .@ to select a shallow value:
let snapshot = identity<User>(reference.@)The same rule applies when references appear inside generic lists, maps, Maybe, Result, channels, callbacks, and struct fields: the reference type remains part of the generic shape.
Explicit Type Arguments
Section titled “Explicit Type Arguments”When type inference isn’t sufficient, provide explicit type arguments:
let label = apply<Int, Str>(42, fn(value: Int) Str { value.to_str() })Type arguments correspond to the order of generics introduced in the signature.
Generics in Anonymous Functions
Section titled “Generics in Anonymous Functions”Anonymous functions may use generic parameters from their enclosing function or receiver method as explicit call type arguments. The generic may be used only in the anonymous function body; it does not need to appear in the anonymous function’s own signature:
fn identity(value: $T) $T { value}
fn callback(value: $T) fn() { fn() { let copy = identity<$T>(value) let _ = copy }}The inherited $T still belongs to callback. Calling the returned anonymous function does not infer or instantiate $T again. Nested anonymous functions may use the same explicit type arguments.
For explicit call type arguments, generics created temporarily while inferring a contextual callback do not declare source names. A nested named function declaration starts a new boundary and cannot use a body-only outer generic as an explicit call type argument.
Generic signatures on local named functions
Section titled “Generic signatures on local named functions”Nested named functions are closure values, so their signatures cannot contain generic parameters. This includes generics belonging to the enclosing declaration:
// Not allowed: the local named function has a generic signature.fn read_value(value: $T) $T { fn read() $T { value } read()}Use an anonymous function when a local closure signature refers to the enclosing generic:
fn reader(value: $T) fn() $T { fn() $T { value }}A local named function with a concrete signature may still capture a generic-typed value for operations that do not expose that type in its signature. Move independently generic named helpers to module scope.
Generic Structs
Section titled “Generic Structs”Structs can also hold generics. If a generic type appears in a field, that field introduces the struct’s generic parameter:
struct Container { value: $T,}
let int_container = Container{value: 42}let str_container = Container{value: "hello"}Generic parameters are introduced by fields and function signatures. Structs may also declare receiver-level generic parameters explicitly when methods need a generic that does not immediately appear in fields:
struct State<$T> { handle: StateHandle,}
impl State { fn value() $T { panic("not implemented") }}Methods may use the generic parameters introduced by their receiver type, but they cannot introduce independent method-only generic parameters.
When referencing a generic struct as a type, provide concrete type arguments:
fn get_value(container: Container<Int>) Int { container.value}