Functions

Functions are the primary way to organize and reuse code in Nitro. They encapsulate behavior and can accept parameters, perform computations, and return values.

Definition and Calling

Basic Function Syntax

Functions are defined using the fun keyword:

// Function with parameters and return type
fun add(a: Int, b: Int): Int {
    return a + b
}

// Function without return value
fun greet(name: String) {
    println("Hello, $name!")
}

// Explicit Nothing return type (optional)
fun print_hello(): Nothing {
    println("Hello, world!")
}

Expression Functions

For simple functions, you can use the special expression syntax:

// Single expression function
fun square(x: Int): Int = x * x

// Important: if the type is not specified, it will be `Nothing`, causing unexpected behavior
fun double(x: Int) = x * 2  // Compile error

Note: This behavior is a limitation of the type inference. The return type must be known before inspecting the function body.

Calling Functions

Call functions by name with arguments in parentheses:

// Basic function calls
let result = add(5, 3)           // result = 8
greet("Alice")                   // prints "Hello, Alice!"
let squared = square(4)          // squared = 16

Module Functions

Functions defined in modules are called using the module path:

// Calling functions from specific modules
let builder = StringBuilder::new()              // Create new StringBuilder
let args = runtime::get_program_args()          // Get command line arguments
let file = runtime::fs::read_text("config.txt")  // Read file from filesystem module

The same applies to constants and types defined in modules.

Generics

Generic functions work with multiple types using type parameters. Type parameters are prefixed with #:

Explicit Generic Declaration

// Explicit type parameter declaration
fun get_value_in_box<#T>(box: Box<#T>): #T {
    return box.value
}

// Generic function with multiple type parameters
fun convert<#From, #To>(value: #From, converter: (#From) -> #To): #To {
    return converter(value)
}

Inferred Generic Parameters

Type parameters can be inferred when used in parameters or return types:

// Type parameter #T is inferred from usage
fun get_value_in_box(box: Box<#T>): #T {
    return box.value
}

// Multiple inferred type parameters
fun create_pair(first: #A, second: #B): Pair<#A, #B> {
    return Pair::of(first, second)
}

Using Generic Functions

let int_box = Box::new(42)
let string_box = Box::new("hello")

// Type inference at call site
let int_value = get_value_in_box(int_box)        // Returns Int
let string_value = get_value_in_box(string_box)  // Returns String

// Explicit type specification (when needed)
let pair = create_pair<String, Int>("age", 25)

Parameters

Function Parameters

Functions can accept multiple parameters with different types:

// Multiple parameters
fun calculate_area(width: Float, height: Float): Float {
    return width * height
}

// Parameters with different types
fun format_message(template: String, count: Int, is_urgent: Boolean): String {
    let urgency = if is_urgent { "URGENT: " } else { "" }
    return "$urgency$template (count: $count)"
}

Return Values

Functions can return values using the return keyword, or implicitly return the last expression:

// Explicit return
fun add_explicit(a: Int, b: Int): Int {
    return a + b
}

// Implicit return (last expression)
fun add_implicit(a: Int, b: Int): Int {
    a + b  // This value is returned
}

// Early return
fun safe_divide(a: Float, b: Float): Optional<Float> {
    if b == 0.0 {
        return None()  // Early return
    }
    Some(a / b)
}

Magic Functions

The language will search the project for functions with specific names to handle certain operations:

Operator Overloading

You can define functions to overload operators like +, -, *, etc.

OperatorFunction NameParametersReturn Type
+plusAnyType, AnyTypeAnyType
-minusAnyType, AnyTypeAnyType
*mulAnyType, AnyTypeAnyType
/divAnyType, AnyTypeAnyType
%remAnyType, AnyTypeAnyType
<=>get_orderingAnyType, AnyTypeOrdering
==is_equalsAnyType, AnyTypeBoolean
^^logical_xorAnyType, AnyTypeBoolean
&&logical_andAnyType, AnyTypeBoolean
||logical_orAnyType, AnyTypeBoolean
!logical_notAnyTypeBoolean
[]getAnyType, IntAnyType
[]!!unsafe_getAnyType, IntAnyType
a[i] =setAnyType, Int, AnyTypeNothing
a[] =addAnyType, AnyTypeNothing
..<range_up_toAnyType, AnyTypeAnyType
..=range_toAnyType, AnyTypeAnyType
<<bitwise_shift_leftAnyType, IntAnyType
>>bitwise_shift_rightAnyType, IntAnyType
>>>bitwise_shift_right_unsignedAnyType, IntAnyType
&bitwise_andAnyType, AnyTypeAnyType
^bitwise_xorAnyType, AnyTypeAnyType
| bitwise_orAnyType, AnyTypeAnyType
?is_returnable_errorAnyTypeBoolean
!!get_or_crashAnyTypeAnyType
for loopto_iteratorAnyTypeAnyType
for loopnext_itemAnyTypeOptional<AnyType>

Example of Operator Overloading

// Overloading the + operator for a custom type
fun Vec2.add(other: Vec2): Vec2 = Vec2::new(this.x + other.x, this.y + other.y)

The only requirement is that the function must be named according to the operator it overloads, and it should accept the appropriate parameters.

Getters and Setters

You can call any getter or setter function using the property syntax:

struct Person { ... }

fun Person.get_full_name(): String = "$first_name $last_name"

fun Person.set_full_name(name: String) {
    let parts = name.split(" ")
    first_name = parts[0]
    last_name = parts[1]
}

person.full_name  // Calls get_full_name()
person.full_name = "John Doe"  // Calls set_full_name("John Doe")

Future Features

Default Arguments: Currently not supported but planned for a future version.

// This will be supported in the future:
// fun greet(name: String, greeting: String = "Hello") { ... }

greet("Alice")  // Prints "Hello, Alice!"