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
Firstwhere aSecondis 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>]insideTreeNode<T>. - If overloads become identical for a type argument, e.g.
describe(_ x: T)anddescribe(_ x: Int)forT = Int, using that type argument is an error.