Skip to main content
View

Generics & Kind Sigils

This page explains the generic type parameters and kind sigils that power TypR's type system.

Free generics​

A free generic is a type parameter that can stand for any type. It is declared with an uppercase letter:

let id <- fn(x: T): T { x };         # T is a free generic
let pair <- fn(a: T, b: U): Tuple[T, U] { :{a, b} };

Free generics have no constraints, any type can be substituted. They are the simplest form of polymorphism in TypR.

Kind sigils​

Kind sigils are prefix characters that constrain what a generic parameter can represent. They encode structural intent directly in the type signature:

SigilNameConstraintExample
#NIndexDimension of an array[#N, int]
$TLabelName of a field$T
%RRecordMust be a Record%R
@IInterfaceMust be an Interface@I
^SStringMust be char^S
?BBooleanMust be bool?B

Index generics (#N)​

The #N sigil represents a dimension, so the size or index of an array. It is used when you need to track or enforce array lengths at the type level:

let head <- fn(v: [#1, T]): T { v[0] };

# Here #N is inferred as 3
let arr <- [1, 2, 3];
let first <- head(arr);   # type-checks: #1 matches the first element

Index generics appear prominently in array and dataframe types:

type Vector <- [#N, int];        # vector of ints, length N
df[N]{ name: char, age: int }    # dataframe with N columns

See Types for more on array and vector syntax.

Label generics ($T)​

The $T sigil represents a field name, which is a compile-time string literal used as a record key:

# $T constrains the generic to a field label
let get_field <- fn(r: %R, key: $T): Any { r[key] };

Label generics are primarily used internally by the compiler to enforce named field access on records.

Record generics (%R)​

The %R sigil constrains a generic to any record type:

let fields_of <- fn(r: %R): char { "record" };

let p <- :{ x = 1, y = 2 };
fields_of(p);   # OK: p is a record

This is useful when a function needs to operate on any record without caring about its specific fields.

Interface generics (@I)​

The @I sigil constrains a generic to any interface type:

type Movable <- interface { mv: (Self, int, int) -> Self };

let move_all <- fn(items: [@I, T], dx: int, dy: int): [@I, T] {
    # T must be an interface implementor
    /* ... */
};

See Interfaces & Structural Validation for how interfaces work.

String generics (^S)​

The ^S sigil constrains a generic to the char type (strings):

let repeat <- fn(s: ^S, n: int): ^S { /* ... */ };

Boolean generics (?B)​

The ?B sigil constrains a generic to the bool type:

let guard <- fn(condition: ?B, value: T): T { /* ... */ };

Combining generics​

Generics can be combined in function signatures to express complex relationships:

@map: (a: [#N, T], f: (T) -> U) -> [#N, U];

Here, three different generic forms appear together:

  • #N: the array dimension (preserved through the map)
  • T: the input element type (free generic)
  • U: the output element type (free generic)

The signature enforces that map preserves array length while allowing element type transformation.

Generics in type definitions​

Generics also appear in type aliases and opaque types:

type Option<T> <- .Some(T) | .None;   # free generic
opaque Factor<L> <- int;               # phantom parameter — L is never used in the body

A phantom parameter (like L above) exists only for type-level tracking. It appears in signatures but has no runtime representation. This pattern is useful for encoding constraints that are checked at compile time only.

See Type Constructors & Aliases for more on type definitions.

Summary​

FormPurposeExample
TFree generic (any type)fn(x: T): T
#NArray dimension / index[#N, int]
$TField label (compile-time string)$T
%RConstrained to Record%R
@IConstrained to Interface@I
^SConstrained to char^S
?BConstrained to bool?B