Tutorial
Tutorial
A hands-on introduction to functions, collections, local state and parallel work.
This tutorial is for programmers learning Kalandra for the first time. It builds from ordinary CPU functions to arrays, numerical tensors and independent tasks. You do not need a GPU to run any example on this page.
Before you begin, install the toolchain and run the getting-started example. Save each example below as a separate main.k, replacing the previous contents. Check and run it using the same commands:
kalandra-check main.k
kalandra main.k -o program
./program On Windows, use -o program.exe and ./program.exe. The text after each example describes the result the executable displays. The reference pages linked at each step explain additional rules and limitations.
Values and function results
A .k file contains function and type declarations. Execution starts at main. Use fn to define a function; the indented body ends with the expression to return. You do not need a return keyword.
fn main() -> int:
width = 6
height: int = 7
width * height This returns 42. The = introduces a name for a value. width has an inferred type; height explicitly declares int. The -> int after main() declares the function's result type. int is a signed 64-bit integer on every supported platform. Use # for a line comment and two spaces for each indentation level.
Define and call a function
Name parameters and their types inside parentheses. Call the function with arguments in the same order:
fn double(value: int) -> int:
value * 2
fn main() -> int:
double(21) This returns 42. A function can also be stored in a binding or passed to another function. You will use that when mapping an array.
Choose a result
An if expression selects one result. Both branches must produce compatible types; only the selected branch runs.
fn absolute(value: int) -> int:
if value < 0:
-value
else:
value
fn main() -> int:
absolute(-42) This returns 42. Comparisons produce Bool values, true or false. Combine conditions with and, or and not. Write a separate comparison on each side of and, such as value >= 0 and value < 100.
Update local state
Reuse = to change a numeric or Bool binding without changing its type. A while loop repeats while its condition is true. Put the function's result after the loop:
fn main() -> int:
index = 0
total = 0
while index < 5:
total = total + index
index = index + 1
total This returns 10: the sum of 0, 1, 2, 3 and 4. Reassigning a scalar parameter inside a function does not change the caller's binding. Arrays have additional rules because two names can refer to the same storage.
Use for value in values to visit array elements, or for index in range(start, stop, step) to count without constructing an array. break exits the nearest loop, and continue advances to its next iteration. Both loop kinds also have an expression form with break value and an else result for normal completion.
Build and transform an array
An array contains elements of one type. [int] is an array of integers. generate creates elements from their indexes; map transforms them in order without changing the input array.
fn double(value: int) -> int:
value * 2
fn main() -> [int]:
values = generate(4, fn(index): index + 1)
map(values, double) This returns [2, 4, 6, 8]. The anonymous function fn(index): index + 1 receives indexes 0 through 3. You can also write values -> map(double): the pipeline passes values as the first argument to map. Indexes start at zero, and length(values) gives the number of elements.
Share an array or copy it
Assigning an array to another name shares its storage. Use copy when changes should be independent:
fn main() -> [int]:
values = [1, 2, 3]
alias = values
independent = copy(values)
values[0] = 9
[alias[0], independent[0]] This returns [9, 1]. alias sees the edit; independent keeps the original value. Fresh numeric and Bool arrays allow local element writes. A function that writes an array supplied by its caller must declare a mut parameter. Ordinary parameters and captured arrays are read-only, and an unfinished task reader prevents writes to shared storage.
For a style that avoids in-place edits, use map, generate, slice, concat and sorted to construct new arrays. sort edits an existing array; sorted returns a new sorted one.
Compute with tensors
A tensor is a dense numerical value with a shape. A matrix has two dimensions. Tensor[float32, 1, 2] means one row and two columns of 32-bit floating-point numbers.
fn main() -> float32:
left: Tensor[float32, 1, 2] = tensor([[2, 3]])
right: Tensor[float32, 2, 1] = tensor([[4], [5]])
reduceSum(matmul(left, right)) This returns 23. matmul computes 2 * 4 + 3 * 5; reduceSum turns the one-element result tensor into a scalar. Tensor inputs stay unchanged. Constructing a tensor does not send work to a GPU: this example runs on the CPU.
Handle an expected error
Declare errors that callers should handle explicitly. A function's ! annotation names its error family. fail produces an error instead of the success value; catch handles it.
error InputError:
Negative
fn requireNonnegative(value: int) -> int ! InputError:
if value < 0:
fail InputError.Negative
value
fn main() -> int:
requireNonnegative(-1) catch fn(error): 0 This returns 0 because the validation fails and the handler provides a fallback. A caller can instead use try to propagate the error through its own declared error channel. Checked execution faults, such as an array index outside its bounds, are distinct from these application-defined errors.
Run independent tasks
Ordinary calls run synchronously. spawn starts a task; await waits for its result. A parallel scope waits for all of its child tasks before it exits. Start independent tasks before awaiting them so they can overlap.
fn double(value: int) -> int:
value * 2
fn main() -> int ! TaskError:
try parallel:
left = spawn double(10)
right = spawn double(11)
a = try await left
b = try await right
a + b This returns 42. Task execution can fail, so main declares ! TaskError and uses try to propagate failures from await and the parallel scope. This tiny example demonstrates the syntax; task scheduling is useful when the independent work is large enough to justify its cost. Use parallelMap when you want CPU workers to map an array while preserving result order.
Where to go next
Use modules to split an application across files, records and enums to model your data, and explicit placement to run supported numerical functions on a GPU. GPU programming adds device requirements and data transfers; it does not change the basic function syntax.