Classes
A class combines data (properties) and behavior (methods) into a single named type. Use the class keyword to declare one.
Declaration
class Vec2 {
x: Float
y: Float
}
Create an instance by calling the class name as a constructor:
var p = Vec2(x: 1.0, y: 2.0)
Member Order
The members of a class must be declared in this order:
- Properties — stored and computed
- Constructors
- Methods
Members of the same kind can be freely interleaved, but the groups cannot: a property declared after a method, or a constructor declared after a method, is a compile error.
class Counter {
count: Int = 0 // 1. properties
doubled: Int {
return self.count * 2
}
constructor(from start: Int) : ( // 2. constructors
count: start,
)
increment() { // 3. methods
self.count = self.count + 1
}
}
Properties
Stored Properties
Stored properties hold a value as part of the instance. A stored property may declare a default value.
class Button {
label: String = "Click Me"
width: Float
height: Float
}
Properties without a default value must be supplied when creating an instance.
Computed Properties
Computed properties derive their value from other properties. They are part of the language's reactivity system: a computed property is re-evaluated automatically when one of its dependencies changes, not on every access.
Read-only
A body written directly after the type creates a read-only computed property:
class Circle {
radius: Float = 1.0
circumference: Float {
return 2.0 * 3.14159 * self.radius
}
}
Read-write
Use explicit get and set blocks to make a computed property writable:
class Slider {
raw_value: Float = 0.0
percentage: Float {
get { return self.raw_value * 100.0 }
set(p) { self.raw_value = p / 100.0 }
}
}
Constructors
Default Constructor
Komplete Script generates a default constructor for every class. It accepts values for all stored properties as named arguments. Properties with default values are optional in the call.
class Vec2 {
x: Float
y: Float
}
var v = Vec2(x: 0.5, y: 1.0)
Custom Constructors
Define a custom constructor with the constructor keyword. It must delegate to the default constructor using : (…) to ensure all stored properties are initialized.
Constructors follow the same parameter rules as functions: parameters can have custom argument labels, be made positional with _, or be given default values. See Functions for the full parameter reference.
class Vec2 {
x: Float
y: Float
}
class BoundingBox {
top_left: Vec2
bottom_right: Vec2
constructor(x: Float, y: Float, width: Float, height: Float) : (
top_left: Vec2(x: x, y: y),
bottom_right: Vec2(x: x + width, y: y + height),
)
}
Adding a custom constructor hides the generated default constructor.
A custom constructor may include a body for additional setup steps:
constructor(x: Float, y: Float, width: Float, height: Float) : (
top_left: Vec2(x: x, y: y),
bottom_right: Vec2(x: x + width, y: y + height),
) {
print("BoundingBox created")
}
Multiple constructors can be defined on the same class, as long as each has a distinct parameter signature:
class Vec2 {
x: Float
y: Float
}
class BoundingBox {
top_left: Vec2
bottom_right: Vec2
constructor(x: Float, y: Float, width: Float, height: Float) : (
top_left: Vec2(x: x, y: y),
bottom_right: Vec2(x: x + width, y: y + height),
)
constructor(top_left: Vec2, bottom_right: Vec2) : (
top_left: top_left,
bottom_right: bottom_right,
)
}
var r1 = BoundingBox(x: 0, y: 0, width: 100, height: 50)
var r2 = BoundingBox(top_left: Vec2(x: 0, y: 0), bottom_right: Vec2(x: 100, y: 50))
Chaining onto a Constructor Call
A constructor call cannot be followed directly by a . access. Wrap the call in parentheses, or assign it to a variable first. This applies to any type, including the ones from the standard library and the ui package.
var faded = (Color(0xFFFFFFFF)).opacity(0.5) // ok — parenthesised
var red = Color(0xFFFF0000)
var channel = red.red // ok — accessed through a variable
var broken = Color(0xFFFFFFFF).opacity(0.5) // error — no direct access on a constructor call
Calls to ordinary functions and methods do not need the extra parentheses — the restriction only applies to constructors.
Methods
Methods are functions declared inside a class body. They have implicit access to self.
class Counter {
count: Int = 0
increment() {
self.count = self.count + 1
}
value() -> (Int) {
return self.count
}
}
Overloading (8.12)
A class can define multiple methods with the same name, as long as their parameters differ - in number, in types, or in argument labels. The compiler selects the matching overload based on the arguments at the call site:
class Playlist {
titles: [String] = []
add(_ title: String) {
self.titles.append(title)
}
add(_ titles: [String]) {
self.titles.append(titles)
}
}
var playlist = Playlist()
playlist.add("Intro") // calls add(_ title: String)
playlist.add(["Verse", "Chorus"]) // calls add(_ titles: [String])
If the arguments match more than one overload, the overload requiring the fewest implicit conversions is selected (8.14). For example, an Int argument matches both an Int and a Float parameter, but only the latter requires a conversion:
class Formatter {
format(_ value: Int) -> (String) {
return "Int: \{value}"
}
format(_ value: Float) -> (String) {
return "Float: \{value}"
}
}
var formatter = Formatter()
print(formatter.format(5)) // Prints "Int: 5"
print(formatter.format(5.0)) // Prints "Float: 5.0"
If several overloads require the same number of conversions, e.g. an Int argument for overloads taking Float and Int?, the call is ambiguous and reported as an error.
Methods that differ only in their return type are not valid overloads. Overloading is only supported for methods. Free functions cannot be overloaded.
String Interpolation
By default, interpolating a class instance into a string produces a representation of its stored properties (computed properties are excluded):
import { sqrt } from math
class Vec2 {
x: Float
y: Float
length: Float {
return sqrt(self.x * self.x + self.y * self.y)
}
}
print("\{Vec2(x: 3.0, y: 4.0)}") // Prints "Vec2(x: 3.0, y: 4.0)"
Define a to_string() -> (String) method to customise this output:
import { sqrt } from math
class Vec2 {
x: Float
y: Float
length: Float {
return sqrt(self.x * self.x + self.y * self.y)
}
to_string() -> (String) {
return "(\{self.x}, \{self.y})"
}
}
print("\{Vec2(x: 3.0, y: 4.0)}") // Prints "(3.0, 4.0)"
self
Inside a class, self refers to the current instance and provides access to all its properties and methods. It is always implicitly available.
Subscript Operator (8.11)
A class can support bracket subscript access (obj[expr]) by defining an at method with exactly one parameter. The parameter name and label are ignored for subscript calls — only the type matters. at is a regular method and can also be called directly:
class NumberList {
values: [Int] = []
at(_ index: Int) -> (Int) {
return self.values[index]
}
}
var list = NumberList(values: [10, 20, 30])
print("\{list[1]}") // Prints "20" — subscript syntax
print("\{list.at(1)}") // Prints "20" — direct method call, equivalent
To also support subscript assignment (obj[expr] = value), add an assign method with exactly two parameters: the assigned value first, then the subscript expression. It must not return a value. Like at, it can also be called directly:
class NumberList {
values: [Int] = []
at(_ index: Int) -> (Int) {
return self.values[index]
}
assign(_ value: Int, at index: Int) {
self.values[index] = value
}
}
var list = NumberList(values: [10, 20, 30])
list[1] = 99 // subscript assignment
list.assign(99, at: 1) // direct method call, equivalent
print("\{list[1]}") // Prints "99"
Generic Classes Experimental
Classes can declare type parameters, e.g. class Box<T> { value: T }, and are then constructed with explicit type arguments, e.g. Box<Int>(value: 5). See Generics for details.
Reference Semantics
Classes have reference semantics: assigning a class instance to a variable or passing it to a function does not copy the instance — both the original and the new variable point to the same object. Mutating a property through one reference is visible through all other references to the same instance.
class Counter {
count: Int = 0
}
var a = Counter()
var b = a // b points to the same instance as a
b.count = 10
print("\{a.count}") // Prints "10" — a and b share the same instance
This makes classes well-suited as shared data models: a single instance can be declared globally or passed between components, and any mutation to it is immediately reflected everywhere that instance is used.