Language Tour
Komplete Script is a type-safe mixed imperative/declarative programming language. This section will cover all language constructs you need to know to get started with Komplete Script.
Comments
A normal line comments starts with two forward slashes (//
). Everything after the two slashes will be part of the comment until the end of the line. Comments across several lines begin with /*
and end in */
.
// A line comment
var name = 5 // Can also be at the end
/*
A comment that spans
multiple lines.
Can also be nested
/*
*/
*/
Printing
Traditionally every first program should print the words “Hello, world!”. In Komplete Script this can be done with the print
function:
print("Hello, world!")
// Prints Hello, world!
If you're familiar with other languages, this might seem familiar. The print
function is always available and doesn't require any additional imports. In the context of Kontakt, this program cannot be used as is and requires an exported main component for the UI. To explore and test examples use the following scaffolding code.
import { Text } from ui
print("Hello, world!")
// Prints Hello, world! to Creator Tools
export var main = Text("My First Instrument")
Don't worry if you don't yet understand what import
or export var main
mean, both will be covered in the following sections.
Variables & Values
Use var
to make a variable. A variable must always be initialized and can be reassigned.
var my_variable = 42
my_variable = 1 // assign 1 to my_variable
A variable must have the same type as the value you assign to it. However, you don't always have to write the type explicitly. The compiler can infer the type based on the assigned value. In the example above the type inferred by the compiler is Int
.
If the initial value doesn't provide enough information, specify the type after the name separated by a colon (:
):
var implicit_int = 42
var implicit_float = 42.0
var explicit_float: Float = 42
Except for Int
, which can implicitly convert to Float
, no other type can implicitly convert to another:
var my_int = 42
var my_float: Float = my_int // ok
my_int = my_float // error, not allowed
var my_str: String = my_int // error, not allowed
If you want to convert any value to a string you can use string interpolation. Simply, write the value in curly braces ({}
) and put a backslash (\
) in front of the opening brace:
var my_str = "\{my_int}"
Create arrays using square brackets ([]
), and access their elements by writing the index in brackets. A trailing comma is allowed after the last element:
var effects = ["Phaser", "Reverb", "Delay"]
effects[1] = "Chorus" // replaces Reverb with Chorus
print("First effect: \{effects[0]}") // Prints "First effect: Phaser"
Arrays automatically grow as you add elements.
effects.append("Bit Crusher")
print("\{effects}") // Prints "[Phaser, Chorus, Delay, Bit Crusher]"
If you're assigning an empty array to a new variable or another place where there isn't any type information, you need to specify the type:
var empty_array: [Int] = []
Create maps using square brackets ([]
) and separate the key with a colon (:
) from the value. Access their elements by writing a key in brackets. A trailing comma is allowed:
var phone_book = [
"Malcom": "012345",
"Jane": "987866",
]
// update Jane's phone number
phone_book["Jane"] = "990022"
// lookup Malcom's phone number
var malcom = phone_book["Malcom"]
if malcom != nil {
print("Malcom's phone number is: \{malcom!}")
}
else {
print("Malcom is not in the phone book")
}
To delete an entry from a map assign nil
to it:
phone_book["Malcom"] = nil
If you're assigning an empty map to a new variable or another place where there isn't any type information, you need to specify the type:
var empty_map: [String: Int] = [:]
Here's a quick overview of all standard types:
var a_bool: Bool = false // or true
var an_int: Int = 42
var a_float: Float = 42.0
var a_string: String = "Hello, world!"
var an_int_array: [Int] = [1, 2, 3]
var a_int_to_string_map: [Int: String] = [1: "A", 2: "B", 3: "C"]
Control Flow
Use if
to make conditionals and for-in
or while
to make loops.
var individual_scores = [75, 43, 103, 87, 12]
var team_score = 0
for score in individual_scores {
if score > 50 {
team_score = team_score + 3
} else {
team_score = team_score + 1
}
}
print("\{team_score}")
// Prints "11"
In an if
statement the condition must be an expression of type Bool
- this means that code such as if score { … }
is an error. There's no implicit comparison against 0
.
You can also use the ternary conditional expression a ? b: c
after the equal-sign (=
) or any place that accepts an expression:
var message = team_score > 100 ? "Congrats! You won!" : "Not enough! Maybe next time."
print("You scored: \{team_score}. \{message}")
Optionals
Sometimes you might not have a value. These values are represented by optionals. An optional value either contains a value or contains nil
to indicate the absence of a value. Write a question mark (?
) after the type to mark it as optional:
var maybe_string: String? = "Hello"
print("has value: \{maybe_string != nil}") // Prints "has value: true"
maybe_string = nil
print("has value: \{maybe_string != nil}") // Prints "has value: false"
Before accessing the value of an optional, you should check whether it has a value by comparing it against nil
. Once you know that a value exists you can get the value by putting an exclamation mark (!
) after the variable name - this is often called unwrapping.
if maybe_string != nil {
print("we have a string: \{maybe_string!}")
} else {
print("we have no string")
}
Unwrapping an optional that is nil
will cause a runtime error.
The result of a ternary conditional expression can also be an optional:
var maybe_string: String? = 0 > 1 ? "Hello" : nil
Functions and Closures
Use fun
to declare a function. Call a function by following its name with a list of arguments enclosed in parenthesis (()
). Use →
to separate the parameter names and types from the function's return type.
fun clip(value: Int, min: Int, max: Int) -> (Int) {
if value > max {
return max
}
if value < min {
return min
}
return value
}
var result = clip(value: 42, min: 1, max: 10)
print("Result: \{result}") // Prints "Result: 10"
By default, functions use their parameter names as labels for their arguments. Write a custom argument label before the parameter name, or write _
to use no argument label.
fun clip(_ value: Int, from min: Int, to max: Int) -> (Int) {
if value > max {
return max
}
if value < min {
return min
}
return value
}
var result = clip(42, from: 1, to: 10)
print("Result: \{result}") // Prints "Result: 10"
The arguments have to follow the parameter definition order if the parameter is positional. If the parameter is named, it can be provided in an arbitrary order. Based on the example above:
clip(42, to: 10, from: 1) // valid
clip(from: 1, 42, to: 10) // invalid
Functions that don't return anything can either use → ()
as return type or simply omit this entirely:
fun say(_ text: String) {
print("Say: \{text}")
}
To return more than one value from a function, list all return types and separate all return values with a comma (,
) after the return
statement.
fun get_name_and_age() -> (String, Int) {
return "Maria", 22
}
var name, age = get_name_and_age()
print("Name: \{name}, Age: \{age}") // Prints "Name: Maria, Age: 22"
Functions can also be nested. Nested functions have access to variables and parameters of the outer function. You can use nested functions to organize code in a function that is long and complex.
fun my_multiplier(value: Int) -> (Int) {
fun multiply_by_ten(_ input: Int) -> (Int) {
return input * 10
}
return multiply_by_ten(value)
}
var result = my_multiplier(value: 10)
print("Result: \{result}") // Prints "Result: 100"
Functions are a first-class type. This means that a function can return another function as its value.
fun make_counter(initial: Int) -> (() ->(Int)) {
var count = initial
fun counter() -> (Int) {
var value = count
count = count + 1
return value
}
return counter
}
var counter = make_counter(initial: 10)
print("\{counter()}, \{counter()}") // Prints "10, 11"
Functions can also be passed as arguments to other functions.
fun find_index(in values: [Int], by condition: (Int) -> (Bool)) -> (Int?) {
var index = 0
for value in values {
if condition(value) {
return index
}
index = index + 1
}
return nil
}
var index = find_index(in: [10, 20, 30, 40], by: fun (value) {
return value == 30
})
print("Index: \{index}")
The argument passed to the by
parameter is called a closure. Closures are a special case of functions. They don't have a name and they don't support argument labels. Any function can be coalesced into a closure.
var find_index_closure = find_index // has type ([Int], (Int) -> (Bool)) -> (Int?)
var result = find_index_closure([1, 2, 3, 4], fun (value) { return value > 2 })
Note that in the case of closures, like with any function expression, the parameter type annotation and return type can be omitted, as exemplified above.
The code in a function or closure has access to things like variables and functions that were available in the scope where it was created, even if it is in a different scope when it’s executed — you saw an example of this already with make_counter
.
Objects and Classes
Classes are structures that can combine data (properties) and functionality (methods). Use class
followed by its name to define a class. A property declaration is a name followed by a type separated with a colon (:
). Methods are like functions, but without the leading fun
keyword.
class Person {
first_name: String = "Maria"
last_name: String = "Philipps"
age: Int = 27
full_name: String {
return "\{self.first_name} \{self.last_name}"
}
is_older(than age: Int) -> (Bool) {
return self.age > age
}
}
To create an instance of a class, put parenthesis (()
) after the class name. Use the dot syntax to access the properties and methods of the instance.
var maria = Person()
maria.age = 34
var full_name = maria.full_name
var is_over_18 = maria.is_older(than: 18)
In this example every property has a default value, therefore we don't need to specify anything when creating an instance. If we want to use other values, we can specify the property values between the parenthesis (()
).
var john = Person(first_name: "John", last_name: "Keller") // we omit age and get the default age of 27
If we didn't provide any defaults in the class definition, we would be required to provide the information when creating an instance.
You might have noticed that full_name
can be used like a property, but has a different definition than first_name
or last_name
. This pattern is called a computed property. After the type you define a getter or both a getter and setter between the curly braces ({}
).
class Navigation {
// normal properties
index: Int = 1
page_count: Int = 10
// computed read only property
is_at_end: Bool {
return self.index == self.page_count - 1
}
// computed property (can be set)
normalized_index: Float {
get { return self.index / (self.page_count - 1) }
set(normalized_index) { self.index = (normalized_index * (self.page_count - 1)).to_int() }
}
}
The self
inside a class's methods or computed properties refers to the instance and provides access to all properties and methods. It is implicitly available and doesn't need to be provided manually like in some other languages.
By default, all stored properties (not computed) must be initialized when a class is created. Komplete Script generates a so-called default constructor for every class. Remember the example above where we created a Person
. In this example we were using the class's default constructor.
You can also add your own constructor to a class. Adding your own constructor will automatically hide the default constructor, so it can no longer be accessed when creating an instance. Every custom constructor needs to delegate to the default constructor in order to initialize all properties. The delegate constructor begins with a colon (:
) and the arguments are provided between parenthesis (()
).
class Point {
x: Float
y: Float
}
class RectangularArea {
top_left: Point
bottom_right: Point
constructor(x: Float, y: Float, width: Float, height: Float) : (
top_left: Point(x: x, y: y),
bottom_right: Point(x: x + width, y: y + height),
)
}
var area = RectangularArea(x: 0, y: 10, width: 100, height: 24)
A custom constructor can also have a body by putting further setup steps between curly braces ({}
):
class RectangularArea {
top_left: Point
bottom_right: Point
constructor(x: Float, y: Float, width: Float, height: Float) : (
top_left: Point(x: x, y: y),
bottom_right: Point(x: x + width, y: y + height),
) {
print("Created")
}
}
Enumerations
An enumeration defines a common type for a group of related values. It allows you to work with these values in a type-safe way. Use enum
to create an enumeration.
enum Effect {
reverb,
delay,
chorus,
flanger,
}
var selected_effect = Effect.reverb
print("Selected: \{selected_effect}") // Prints "Selected: reverb"
Components
Components are the building blocks for your UI. Similar to classes they can have properties and methods, but their semantics are quite different as you'll learn in this section. To create a component use component
followed by its name:
import { Text } from ui
component Welcome {
Text("Hello, world!")
}
export var main: Component = Welcome()
Loading this example with display the text “Hello, world!” in the middle of the user interface. Components must always contain other components to build up the UI, they cannot be empty. For our Welcome
component we're using the Text
component provided by the ui
module.
With our first component in place, let's explore properties that enable the customization of a component's appearance or behavior. At the moment the Welcome
component will always display the same text. To allow users of this component to customize the message, you need to add a property. Define a property with @property
followed by its name and type.
import { Text } from ui
component Welcome {
@property text: String
Text(self.text)
}
export var main: Component = Welcome(text: "Hello, Komplete Script!")
Similar to classes, you refer to members of the component through self
and you pass all properties as arguments when creating them. Unlike classes, you cannot refer to properties outside of a component.
var welcome = Welcome(text: "Hello")
var text = welcome.text // doesn't work
Properties allow you to let information flow from the parent to the child component. Therefore, properties are read-only and cannot be changed from within the component.
component Welcome {
@property text: String
// This is an internal method
change_text() {
self.text = "Changed" // error, not allowed
}
Text(self.text)
}
To let information flow upwards you can use callbacks, which are essentially function properties.
import { Text, TapGesture, TapGestureEvent } from ui
component Button {
@property text: String
@property action: (Bool) -> () // Callback
Text(self.text) with {
TapGesture(fun (event: TapGestureEvent) {
self.action(event.modifiers.shift) // Call function property and pass information back up
})
}
}
component Main {
Button(text: "Click Me", action: fun (shift_pressed: Bool) {
print("Got clicked with shift: \{shift_pressed}")
})
}
export var main: Component = Main()
This example already uses a modifier called TapGesture
. You can ignore this detail for now as they will be explained in more depth in the next section. For now, you only need to understand that with { TapGesture(…) }
allows you to receive tap events on a component.
Since properties are read-only you might wonder how they can change over time. Passing a property to a component is actually an expression. If this expression references a state it will automatically update whenever that state changes. A state is simply a name, type and initial value.
import { Text, TapGesture, TapGestureEvent } from ui
component Button {
pressed: Bool = false
// Text will display either "Pressed" or "Not Pressed" based on the value of self.pressed
Text( self.pressed ? "Pressed" : "Not Pressed" ) with {
TapGesture(
down: fun (event: TapGestureEvent) {
self.pressed = true // Change state to true
},
up: fun (event: TapGestureEvent) {
self.pressed = false // Change state to false
}
)
}
}
export var main: Component = Button()
The gestures event callbacks allow us to change the state whenever the user interacts with the Button
. If you run this example you can observe how the displayed text changes when you tap down and then up on the Button
.
You can also create computed state, similar to computed properties in classes.
import { Text, TapGesture, TapGestureEvent } from ui
component Button {
pressed: Bool = false
text: String {
if self.pressed {
return "Pressed"
}
return "Not Pressed"
}
// Text will display either "Pressed" or "Not Pressed" based on the value of self.pressed
Text(self.text) with {
TapGesture(
down: fun (event: TapGestureEvent) {
self.pressed = true // Change state to true
},
up: fun (event: TapGestureEvent) {
self.pressed = false // Change state to false
}
)
}
}
export var main: Component = Button()
Computed states allow you to organize your code in a different way.
States are internal to a component and can only be referenced by the component itself, but sometimes it can be useful to allow a child component to get control over a state. This is called a binding. To accept a binding as argument to your component use @binding
followed by its name and type.
component Checkbox {
@binding checked: Bool
Rectangle(
color: self.checked ? Color(0xFFFFFFFF) : Color(0x66FFFFFF)
) with {
TapGesture(fun (event: TapGestureEvent) {
// Toggle checked state
self.checked = not self.checked
})
}
}
The Checkbox
receives a binding to a bool that it toggles from the TapGesture
. The Rectangle
indicates the checked state by adapting its color. Unlike properties, a binding can be mutated by assigning to it. To pass a state as a binding to a component use a $
in front of the state's name.
component Main {
checked: Bool = false
Checkbox(checked: self.$checked) // Pass binding to checked state
}
Here's the complete example
import { Rectangle, TapGesture, TapGestureEvent } from ui
component Checkbox {
@binding checked: Bool
Rectangle(
color: self.checked ? Color(0xFFFFFFFF) : Color(0x66FFFFFF)
) with {
TapGesture(fun (event: TapGestureEvent) {
// Toggle checked state
self.checked = not self.checked
})
}
}
component Main {
checked: Bool = false
Checkbox(checked: self.$checked) // Pass binding to checked state
}
export var main: Component = Main()
Finally, let's have a look at injecting components into others to build up hierarchies. If a component supports children, they can be injected by providing a trailing block containing all components that are supposed to be injected. For an example we use HStack
which is a container component provided by the ui
package that lays out its children on a horizontal line.
import { HStack, Rectangle } from ui
component Main {
HStack(spacing: 8) {
// Here are the children
Rectangle(color: Color(0xFFFF0000))
Rectangle(color: Color(0xFF00FF00))
Rectangle(color: Color(0xFF0000FF))
}
}
export var main: Component = Main()
Which results in this
Modifiers
A modifier you can apply to a component to produce a different version of the original value.
Modifiers are applied to a component by following it with a with
block.
import { Text, Rectangle, Padding, Background } from ui
export var main = Text("I am modified") with {
Padding(8) // Pad all sides of text equally before applying the background
Background { // Add a red background to the text
Rectangle(color: Color(0xFFFF0000))
}
}
The order in which modifiers are applied matters. If we would swap Padding
and Background
the result would look like this.
Now the background is applied to the text directly and the padding is added after it. The same modifier can also be applied multiple times. By adding another Background
after Padding
we can see that Padding
still applies after the first Background
.
import { Text, Rectangle, Padding, Background } from ui
export var main = Text("I am modified") with {
Background { // Add a red background to the text
Rectangle(color: Color(0xFFFF0000))
}
Padding(8) // Pad all sides of text equally after applying the background
Background { // Add a green background to the text
Rectangle(color: Color(0xFF00FF00))
}
}
The ui
package contains plenty of modifiers for you to explore. Some affect the layout, others allow you to respond to user input gestures and more.
You can also create your own modifiers by using the modifier
keyword followed by a name and body. The modifier syntax is equivalent to component, meaning it also supports properties, bindings and states. Unlike component it gets access to the component it was applied to through the child
variable.
import { Padding } from ui
modifier LargePadding {
child with {
Padding(24)
}
}
When applied this modifier will apply the Padding
modifier from the ui
package with a value of 24
pixels.
Templates
A Template
encapsulates a set of components. Components inside of a template aren't created immediately, but only when the template is invoked. In fact, a template can be invoked multiple times to create several components from the same template. Furthermore, templates can also be parameterized, which makes them a powerful tool for designing flexible components.
The HStack
example at the end of the section injected three components into the container by using a trailing block after the constructor. This trailing block is simply a template without parameters. Let's make our own container component that arranges its children inside of an HStack
with a pre-defined spacing.
import { HStack, Rectangle } from ui
component MyContainer {
@property children: Template() // Adding this allows us to use the trailing block
HStack(spacing: 16) {
self.children() // Place children inside of HStack
}
}
// Because MyContainer has the "children: Template()" property we can use a trailing block to inject children like
// a container.
export var main: Component = MyContainer {
Rectangle(color: Color(0xFFFF0000))
Rectangle(color: Color(0xFF00FF00))
Rectangle(color: Color(0xFF0000FF))
}
Combined with a for-loop templates can be used to create powerful list components.
import { VStack, Text } from ui
component MyList {
@property data: [String] // The array containing the text for each item
@property item: Template(String) // The template for creating a list item passing over its text
VStack {
// Iterate of each item in data and create a list item with its text for it
for text in self.data {
self.item(text)
}
}
}
export var main: Component = MyList(
data: ["Reverb", "Delay", "Chorus"],
item: template (text) {
Text(text)
}
)
The result of a template invocation is always Component
. Inside of a template you can use a special if
and for
statement designed for creating component hierarchies. We already made use of the declarative for-loop
in the example above. Unlike in normal imperative code, we don't need to add each list item to a result. Here's a comparison.
// imperative loop
var list: [Component] = []
for text in ["Reverb", "Delay", "Chorus"] {
// During each iteration we append an item to the resulting list array
list.append(Text(text))
}
// declarative loop
VStack {
for text in ["Reverb", "Delay", "Chorus"] {
Text(text) // automatically added to VStack
}
}
Import & Export
A file in Komplete Script is called a module and has the extensions .kscript
. Modules can export a variety of things which then can be imported by other modules. They allow you to organize your code into several files & folders.
Everything that you can define in a module can be exported, e.g. a variable or a class.
Assume the following definition of my_module.kscript
import { Text, Padding } from ui
export var title: String = "Language Tour"
export enum Page {
main,
settings,
fx,
}
export class Model {
current_page: Page = Page.main
}
export component LargeText {
@property text: String
Text(self.text, size: 48)
}
export modifier LargePadding {
child with {
Padding(48)
}
}
export type PageID = Int
// You can even re-export from another module as part of this module
export { Rectangle } from ui
To import something from another module use the import
statement.
import { title, Page } from my_module
You can also import everything from a module by using a wildcard *
import * from my_module
You can also import an entire module into a namespace to avoid clashes or help indicate from which module a certain symbol or type comes from.
import * as my from my_module
print("\{my.title}") // Refer to the exported title by prefixing its access with the namespace "my"
The module names have to start with a lowercase letter and can contain letters, underscore(_) and numbers. Other characters are not valid.
You can import from modules in the same directory or in sub-directories, as long as the directory names follow the module naming scheme(lowercase letters and _). Here is an example of a project structure:
Resources/komplete_scripts/
main.kscript
components/
tabbar.kscript
kontakt_components/
button.kscript
base/
button_base.kscript
In the main.kscript
file you could import from the submodules like so:
import * from components.tabbar
import * from kontakt_components.button
import * from kontakt_components.base.button_base