Skip to content

Functions

Functions allow the encapsulation of reusable logic, returning values, and managing program flow. In CKSP, functions are similar to those in many other programming languages and help reduce repetition, organize code, and improve readability. In software development, it is a best practice to design functions that perform a single task and do it well. This is known as the Single Responsibility Principle (SRP). By adhering to this principle, functions become easier to understand, test, and maintain.

This section introduces function definitions in CKSP, explains handling return values, and covers special cases such as parameterless functions, multiple returns, discardable return values, and function calls in conditional statements.


Function Definitions

A function in CKSP is a block of reusable code that performs a specific task. A function is defined using the function keyword, followed by a name, a pair of parentheses (with or without parameters), and a code block that is executed when the function is called.

Simple function definition
1
2
3
function return_sth()
    return 1
end function

This simple function return_sth requires no parameters and returns the integer value 1.


Calling Functions

To use a function, it is simply called by its name followed by parentheses. If the function returns a value, it can be stored in a variable:

declare get_sth: int := return_sth()  // Calls the function and stores the result in get_sth

Functions with Parameters

Functions in CKSP can accept parameters, which can also have type annotations. This is useful for creating more complex and flexible functions that operate on different inputs.

Function with parameters and type annotations
1
2
3
4
5
function multiply(a: int, b: int) : int
    return a * b
end function

declare result: int := multiply(5, 4)  // Calls the multiply function with 5 and 4

In this example, the multiply function takes two integer parameters, a and b, and returns their product. The return type int is specified in the function signature using a colon :.

This function only works with integer values, and the compiler will raise a type error if real numbers are used. To make the multiply function more flexible, you can use the union type number:

Function with parameters and union type
1
2
3
4
5
function multiply(a: number, b: number) : number
    return a * b
end function

message(multiply(5, 4), multiply(5.3, 7.3))

You can also define functions with multiple parameters of different types:

Function with multiple parameters
1
2
3
4
5
6
7
8
9
function greet(name: string, times: int) : string
    declare greeting: string := ""
    for i in range(times)
        greeting := greeting & "Hello, " & name & "!\n"
    end for
    return greeting
end function

message(greet("Alice", 3))

Here, the greet function accepts a string (name) and an integer (times) as parameters. It returns a string that repeats the greeting the specified number of times.

Function Parameter Types

Primitive function parameter types at your disposal are: int, real, string, as well as the union types number and any. Just like in declarations, you can annotate composite types (arrays or multidimensional arrays) with their element type and putting [] after the type name. Repeat the [] for each dimension of the array.


Parameter Passing

0.0.7

Understanding how parameters are passed to functions is crucial, as it affects how changes within a function impact variables outside of it. CKSP employs different strategies based on the data type:

Passing Primitive Types

When you pass a primitive type (like int, real, string) to a function, CKSP uses Pass-by-Value. This means a copy of the value is made and given to the function's parameter. Any modifications to the parameter within the function will only affect this local copy and will not change the original variable outside the function.

Passing primitive types by value
1
2
3
4
5
6
7
8
function add_one(num: int)
    num += 1 // Changes only the local copy of 'num'
    message("Inside function: " & num)
end function

declare my_number: int := 5
add_one(my_number)
message("Outside function: " & my_number)

Output:

Output: Outside function: 5

Passing Arrays

Unlike primitive types, when you pass an array to a function in CKSP, it is handled using a Pass-by-Reference-like semantic. This means that the function parameter directly refers to the original array in memory. Consequently, any changes you make to the elements or the structure of the array inside the function will directly affect the original array outside the function.

This behavior is chosen for performance reasons, as it avoids time-consuming copy operations for potentially large arrays. To achieve this, the compiler automatically inlines functions that accept array parameters. This means the function's code is directly inserted at the call site, allowing direct manipulation of the original array.

Passing arrays by reference
1
2
3
4
5
6
7
8
function add_element(arr: int[])
    arr[num_elements(arr) - 1] := 100 // Modifies the original array
    message("Inside function (mutated array): " & arr)
end function

declare my_array[3]: int[] := [1, 2, 3]
add_element(my_array)
message("Outside function (after array mutation): " & my_array)

Output:

Output: Outside function (after array mutation): [1, 2, 100]
If you need an independent copy of an array within a function, you must explicitly create one by iterating through its elements and copying them into a new array.

Passing Pointer Variables

When you pass a pointer variable (which references a struct instance) to a function in CKSP, it is also handled using a Pass-by-Reference-like semantic. Here, a copy of the reference (the pointer itself) to the original struct instance is passed to the function's parameter. Although the pointer itself is copied, both the original variable and the function's parameter point to the same underlying struct instance in memory. Consequently, any changes you make to the fields of the struct instance inside the function will directly affect the original struct instance outside the function. This approach is similar to how objects are passed in languages like Python or JavaScript.

Passing pointer variables by reference
struct Point
    declare x: int
    declare y: int

    function move(self, x: int, y: int)
        self.x := self.x + x
        self.y := self.y + y
        message("Inside function (moved point): X=" & self.x & ", Y=" & self.y)
    end function
end struct

on note
    declare my_point: Point := Point(5, 10)
    my_point.move(10, 20)
    message("Outside function (after point mutation): X=" & my_point.x & ", Y=" & my_point.y)
end on

Output:

Output: Outside function (after point mutation): X=15, Y=30
If you need an independent copy of a struct instance, you must explicitly create a new instance and copy its fields.

Passing UI Control Variables

UI control variables (e.g., declare ui_slider, declare ui_button) are a special case in CKSP due to how the underlying KSP engine interacts with them. To perform operations on these controls, KSP often requires the original variable name of the control, not just its value.

Therefore, when you pass a UI control variable to a function, CKSP treats it with Pass-by-Reference-like semantics, similar to arrays and pointer variables. This ensures that any operations performed on the parameter within the function directly affect the original UI control. The compiler achieves this by automatically inlining any function that accepts a UI control variable as a parameter, substituting the formal parameter with the actual variable name from the call site. This guarantees that built-in KSP commands like get_ui_id() receive the correct identifier for the control.

The ref Keyword

For primitive types, you can explicitly force Pass-by-Reference behavior using the ref keyword in the parameter declaration. Any changes to a ref parameter inside the function will directly modify the original variable outside the function. Similar to array and pointer parameters, functions with ref parameters are also inlined by the compiler.

Passing primitive types by reference
1
2
3
4
5
6
7
8
function increment_ref(ref val: int)
    val += 1 // Directly modifies the original 'my_value'
    message("Inside function (ref): " & val)
end function

declare my_value: int := 10
increment_ref(my_value)
message("Outside function (after ref): " & my_value) // Output: Outside function (after ref): 11

When using ref with pointer variables, you can force the function to modify the pointer itself, skipping the process of assigning it to an intermittent parameter variable and having to call reference counting functions.


Functions with multiple Return Statements

0.0.7

In CKSP, functions can use return statements to exit a function and provide a value back to the caller. When a return statement is encountered, the function's execution stops, and the specified value is returned. If multiple return statements exist within a function, the first one that is reached will determine the exit point of the function.

Function with multiple return statements
function search_matrix(matrix: int[][], target: int): int
    declare row, col: int
    for row := 0 to num_elements(matrix, 1) - 1
        for col := 0 to num_elements(matrix, 2) - 1
            if matrix[row, col] = target
                return row * num_elements(matrix, 2) + col  // loop gets stopped and function is exited
            end if
        end for
    end for

    return -1  // If the target is not found
end function

In this example, the search_matrix function searches a 2D array matrix for a target value. It iterates over each row and column, and if the target is found, the function immediately returns a calculated integer value, which is derived from the row and column indices. This early exit allows the function to return the result without needing to complete all iterations.

If the target is not found after checking the entire matrix, the function reaches the final return -1 statement, indicating the absence of the target value. Using return statements in functions allows efficient and immediate exits from the function's logic, especially when a specific condition is met, ensuring that unnecessary code execution is avoided.


Using Multiline Functions In-line

0.0.6

Multiline functions can be used anywhere without needing to store their return values. The compiler handles this automatically.

In control flow statements, the return values of functions can be used in-line without storing them.

Calling functions in an if statement
1
2
3
4
5
6
7
declare matrix[3,3]: int[][] :=  [[1,2,3],    
                                  [4,5,6], 
                                  [7,8,9]]

if search_matrix(matrix, 3) = 2 and search_matrix(matrix, 1) = 0
    message("Unecessary search completed")
end if

In the above example, the message "Unecessary search completed" will be displayed if the function search_matrix returns 2 for the value 3 and 0 for the value 1. This is a simple way to check multiple conditions in a single statement by calling multiline functions inline.

1
2
3
4
5
6
7
8
9
declare matrix[3,3]: int[][] :=  [[1,2,3],    
                                  [4,5,6], 
                                  [7,8,9]]

if search_matrix(matrix, 3) >= 0
    message("Value 3 found in the matrix")
else
    message("Value 3 not found in the matrix")
end if

In this example, the search_matrix function is used in an if statement to check if the value 3 is present in the matrix. If the function returns a value greater than or equal to 0, it means the value 3 was found, and a message is displayed. Otherwise, a different message is shown indicating the value was not found.


Short-Circuit Evaluation

experimental

When using functions in if statements, especially functions with side effects (like logging a message or changing a variable), it's crucial to control when they execute. CKSP uses Short-Circuit Evaluation for the and and or logical operators to optimize performance and provide predictable behavior. This means the evaluation of a conditional chain stops as soon as the final outcome is determined.

In an L and R expression, the right side (R) is only evaluated if the left side (L) is true. This is useful for preventing errors, such as checking if an array index is valid before accessing the element at that index.

Short-Circuiting with 'and'
function is_index_valid(arr: int[], index: int) : int
    message("Checking index " & index & "...")
    if index >= 0 and index < num_elements(arr)
        return 1
    else
        return 0
end function

function is_value_special(arr: int[], index: int) : int
    message("Checking value at index " & index & "...")
    if arr[index] = 42
        return 1
    else
        return 0
end function

declare my_array[3]: int[] := [10, 20, 30]

// Attempt to access an invalid index
if is_index_valid(my_array, 5) = 1 and is_value_special(my_array, 5) = 1
    message("Special value found!")
end if

Output:

Checking index 5...

Because is_index_valid() returns 0, the is_value_special() function is never called, preventing a potential runtime error.

In an L or R expression, the right side (R) is only evaluated if the left side (L) is false. This is ideal for checking for one of several conditions where any single true result is sufficient.

Short-Circuiting with 'or'
function has_critical_error() : int
    message("Checking for critical errors...")
    return 1 // Simulates finding an error
end function

function needs_manual_reset() : int
    message("Checking for manual reset...")
    return 1 // Simulates needing a reset
end function

// Check system status
if has_critical_error() = 1 or needs_manual_reset() = 1
    message("ATTENTION: System requires intervention!")
end if

Output:

Checking for critical errors...
ATTENTION: System requires intervention!

Since has_critical_error() returns 1, the needs_manual_reset() function is skipped, making the code more efficient.

Note that not all logical operators use short-circuiting:

  • xor: Never short-circuits. To determine the result, both sides are always evaluated.
  • not: Simply inverts the result of its operand. The operand itself is always fully evaluated first.

Using Functions with Initializer Lists

0.0.7 experimental

Initializer-Lists allow arrays to be passed directly as parameters without needing to declare them separately. This makes function calls more concise and improves code readability.

Function with initializer list as argument
function sum_elements(arr: int[]): int
    declare total: int := 0
    for value in arr
        total := total + value
    end for
    return total
end function

// Calling the function with an initializer list
declare result: int := sum_elements([1, 2, 3, 4, 5])
message("The sum is: " & result)  // Output: The sum is: 15

In this example, the function sum_elements calculates the sum of all elements in an array. By using an initializer list [1, 2, 3, 4, 5], the array can be passed directly to the function without requiring a separate declaration.