Skip to main content

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:

  1. Properties — stored and computed
  2. Constructors
  3. 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),
)
}
info

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.