Skip to main content

Generics Experimental

Classes, components, and modifiers can be generic: they declare type parameters, which are placeholders for types that are only chosen where the class, component, or modifier is used. This lets you write a single list component that works with presets, samples, or any other type, while the compiler still checks that every use is type-safe.

Declaration​

Type parameters are listed in angle brackets after the name. Like all type names, they must start with an uppercase letter:

class Box<T> {
value: T
}

class Pair<First, Second> {
first: First
second: Second
}

Within the declaration, a type parameter can be used like any other type: for properties, method parameters and return types, optionals (T?), arrays ([T]), map values ([String: T]), and function types ((T) -> ()).

Usage​

When constructing a generic class, component, or modifier, the type arguments must always be given explicitly. They are not deduced from the constructor arguments:

var box = Box<Int>(value: 5)
var pair = Pair<String, Float>(first: "Gain", second: 0.5)

print("\{box.value + 1}") // Prints "6"
print(pair.first) // Prints "Gain"

The same applies to type annotations. Box<Int> and Box<Bool> are different types, and a bare Box is not a valid type:

var other: Box<Int> = box

fun unbox(_ box: Box<Int>) -> (Int) {
return box.value
}

Forwarding Values​

Inside a generic declaration, nothing is known about a type parameter, so a value of type T can only be forwarded: stored, passed to a function or template, returned, or added to and taken out of an array or map.

class Stack<Element> {
_items: [Element] = []

count: Int {
return self._items.length
}

push(_ element: Element) {
self._items.append(element)
}

pop() -> (Element?) {
return self._items.pop_last()
}
}

var names = Stack<String>()
names.push("Alpha")
names.push("Beta")
print(names.pop()!) // Prints "Beta"
print("\{names.count}") // Prints "1"

Any other operation on a value of a type parameter is an error, regardless of the type arguments used:

  • Accessing members, e.g. value.name
  • Comparing values with == or !=, or using functions that rely on comparison, e.g. contains
  • Using a type parameter as a map key, e.g. [T: Int]
  • Converting to a string, e.g. "\{value}"
  • Assigning a concrete value, e.g. value: T = 5
  • Mixing different type parameters, e.g. returning a First where a Second is expected

Comparing an optional T? against nil is allowed.

To do something with a value of a type parameter, let the user of the generic type provide the operation, for example as a function or template property. This is what makes generics useful for components.

Generic Components​

A typical use case is a list component, which arranges items of any type and lets the caller decide how each item is displayed through a template:

import { VStack, Text, TapGesture } from ui

component List<Item> {
@property items: [Item]
@property row: Template(Item)
@property on_select: (Item) -> () = fun (item) {}

VStack {
for item in self.items {
self.row(item) with {
TapGesture(fun (event) {
self.on_select(item)
})
}
}
}
}

class Preset {
name: String
}

export var main: Component = List<Preset>(
items: [Preset(name: "Warm Pad"), Preset(name: "Bright Lead")],
row: template (preset) {
Text(preset.name)
},
on_select: fun (preset) {
print("Selected \{preset.name}")
}
)

List never inspects its items. It only forwards each Item to the row template and the on_select callback, which are provided by the caller who knows that the items are of type Preset.

Generic Modifiers​

Modifiers can be generic in the same way:

import { Text, TapGesture } from ui

modifier Selectable<Value> {
@property value: Value
@property on_select: (Value) -> ()

child with {
TapGesture(fun (event) {
self.on_select(self.value)
})
}
}

export var main: Component = Text("Preset 3") with {
Selectable<Int>(value: 3, on_select: fun (index) {
print("Selected preset \{index}")
})
}

Limitations​

  • Only classes, components, and modifiers can be generic. Functions and methods can't declare their own type parameters.
  • A generic class can't store an array of its own type, e.g. children: [TreeNode<T>] inside TreeNode<T>.
  • If overloads become identical for a type argument, e.g. describe(_ x: T) and describe(_ x: Int) for T = Int, using that type argument is an error.