Nitro Language Documentation
Nitro is a modern, statically typed compiled language designed for performance and simplicity. It combines familiar syntax patterns from languages like Rust, Kotlin, and TypeScript with a focus on readable, maintainable code.
Key Features
- Static typing with type inference for better reliability
- Familiar syntax inspired by modern programming languages
- Powerful type system with generics, options, and traits
- Built-in collections no need to reinvent the wheel
Quick Start
Hello World
Here's your first Nitro program:
fun main() {
println("Hello, World!")
}
Try Online
You can experiment with Nitro code in your browser using the Nitro Playground.
More Examples
Explore additional examples in the src/examples folder of the repository to see Nitro in action.
What's Next?
This documentation will guide you through:
- Getting Started - Basic syntax, variables, and language fundamentals
- Expressions and Operators - Working with values and operations
- Functions - Defining and calling functions, including generics
- Collections - Lists, maps, sets, and other data structures
- Types - Structs, options, enums, and the type system
Ready to learn Nitro? Start with Getting Started!
Getting Started
You can learn Nitro quickly, especially if you're familiar with modern programming languages like Rust, Kotlin, or TypeScript. This chapter covers the fundamental syntax and language features you need to start writing Nitro code.
Variables and Constants
Variables in Nitro are declared using the let keyword. By default, variables are mutable:
let name = "Alice"
let age = 25
let height = 5.6
age = 26 // Can be changed
Except for constants, when you use let on a global scope, it becomes a constant. Constants are immutable and must be
initialized at declaration:
let counter = 0
// counter = counter + 1 // Compile time error: cannot assign to constant
Nitro has powerful type inference, so you often don't need to specify types explicitly. However, you can when needed:
let score: Int = 100
let message: String = "Hello, Nitro!"
Comments
Single line comments start with // and comment the rest of the line.
// This is a single line comment
Multi-line comments start with /* and end with */. They can span multiple lines.
/*
This
is
a
multi-line
comment
*/
Nested multi-line comments are allowed.
/*
* This line opens a multi-line comment
* Also /* nested comments are allowed */ the '* /' did not close the comment
*
*/
Keywords and Identifiers
Reserved Keywords
These words are reserved and cannot be used as variable or function names:
this This fun let mod struct ret size_of option internal rec tag defer type_alias
enum nothing when match alias if else for in while repeat loop is as true false
null include break continue use mut json! test! include_as_bytes! include_as_string!
Note: Some keywords like null are reserved for future use even though they're not currently part of the language.
Valid Identifiers
Identifiers name variables, functions, types, and other entities. Rules:
- Must start with a letter (a-z, A-Z)
- Can contain letters, digits, and underscores
- Cannot start or end with an underscore
- Are case-sensitive
foo // ✓ Valid
fooBar // ✓ Valid (camelCase)
BarFoo // ✓ Valid (PascalCase)
foo_bar // ✓ Valid (snake_case)
baz42 // ✓ Valid (with numbers)
ALL_CAPS // ✓ Valid (constants)
_invalid // ✗ Invalid (starts with underscore)
invalid_ // ✗ Invalid (ends with underscore)
42invalid // ✗ Invalid (starts with number)
The language uses PascalCase for type names, snake_case for variables, constants, and functions.
Syntax Rules
Semicolons and Newlines
Semicolons are optional in Nitro, newlines serve the same purpose. Use semicolons when you want multiple statements on one line:
// These are equivalent:
let a = 1
let b = 2
let c = 3
// Same as:
let a = 1; let b = 2; let c = 3
Commas
Commas are optional in most contexts. Use them for clarity, especially in single-line collections:
// Single line - commas recommended
let numbers = [1, 2, 3, 4, 5]
// Multi-line with commas
let fruits = [
"apple",
"banana",
"cherry", // trailing comma is fine
]
// Multi-line without commas (also valid)
let colors = [
"red"
"green"
"blue"
]
Omitting commas is allowed but not recommended outside DSLs (Domain Specific Languages).
For example, in a DSL like JSON:
json! {
name: "Alice"
age: 30
is_student: false
}
That is valid Nitro code, and it will create a JSON object, isn't that neat?
Expressions and Operators
Expressions are the building blocks of Nitro programs - pieces of code that evaluate to a value. This chapter covers literals, operators, and how to combine them to create meaningful computations.
Literals
Literals are values written directly in your code. Nitro supports several types of literals:
Numbers
// Integers
let positive = 42
let negative = -23
let large = 1_000_000 // underscores for readability
// Floating point
let pi = 3.1416
let explicit_float = 5.0f
let scientific = 1e10 // 10,000,000,000
Strings and Characters
// Strings
let greeting = "Hello, world!"
let multiline = "
This is a
multi-line string
"
let raw_string = r#"This is a raw string with no escape sequences: \n"#
// Characters (Unicode)
let letter: Char = u"A"
let unicode_hex: Char = u"\x41" // 'A' in hex
let unicode_full: Char = u"\u0041" // 'A' in Unicode
// Characters ascii
let ascii_char: Int = a"A" // ASCII character 'A' (65 in decimal)
let ascii_hex: Int = a"\x41" // ASCII character 'A' in hex
Other Literals
// Boolean values
let is_ready = true
let is_finished = false
// Nothing (absence of value)
let empty = nothing
Function Calls
Functions are called using their name followed by arguments in parentheses:
// Built-in functions
println("Hello, world!") // Print with newline
print("No newline") // Print without newline
// Functions with multiple arguments
let result = add(5, 3) // Custom function
let max_val = max(10, 20, 15) // Variable arguments
Note: See the Functions chapter for details on defining your own functions.
Operators
Nitro provides a comprehensive set of operators for different types of operations:
Arithmetic Operators
let a = 10
let b = 3
let sum = a + b // Addition: 13
let diff = a - b // Subtraction: 7
let product = a * b // Multiplication: 30
let quotient = a / b // Division: 3
let remainder = a % b // Reminder: 1
Note: '%' is the remainder operator, not modulo. For modulo behavior you can use
a.modulo(b)that will always return a non-negative result.
Comparison Operators
let x = 5
let y = 10
let equal = x == y // Equality: false
let not_equal = x != y // Inequality: true
let less = x < y // Less than: true
let greater = x > y // Greater than: false
let less_equal = x <= y // Less or equal: true
let greater_equal = x >= y // Greater or equal: false
let ordering = x <=> y // Value comparison: Ordering::Less, Ordering::Equals or Ordering::Greater
Logical Operators
let a = true
let b = false
let and_result = a && b // Logical AND: false
let or_result = a || b // Logical OR: true
let xor_result = a ^^ b // Logical XOR: true
let not_result = !a // Logical NOT: false
// Also valid
let and_result = a and b // Logical AND: false
let or_result = a or b // Logical OR: true
let xor_result = a xor b // Logical XOR: true
let not_result = not a // Logical NOT: false
Bitwise Operators
let x = 12 // Binary: 1100
let y = 10 // Binary: 1010
let and_bits = x & y // Bitwise AND: 8 (1000) also x.bitwise_and(y)
let or_bits = x | y // Bitwise OR: 14 (1110) also x.bitwise_or(y)
let xor_bits = x ^ y // Bitwise XOR: 6 (0110) also x.bitwise_xor(y)
let left_shift = x << 2 // Left shift: 48
let right_shift = x >> 2 // Right shift: 3
let unsigned = x >>> 2 // Unsigned right shift: 3 (same as >> for positive numbers)
// There is no operator for bitwise NOT, but you can use the method:
let not_bits = x.bitwise_not() // Bitwise NOT: -13 (0b11111111111111111111111111110011)
Assignment Operators
let value = 10
value += 5 // value = value + 5 -> 15
value -= 3 // value = value - 3 -> 12
value *= 2 // value = value * 2 -> 24
value /= 4 // value = value / 4 -> 6
value %= 3 // value = value % 3 -> 0
Special Operators
// Type checking and casting
let value = 42
let is_int = value is Int // Type checking: true
let not_string = value !is String // Negative type check: true
let as_int = value as Int // Type casting, throws error if not possible
// Membership testing
let numbers = [1, 2, 3, 4, 5]
let contains = 3 in numbers // Contains: true
let not_contains = 6 !in numbers // Not contains: true
// Null coalescing (default values)
let optional_value: Optional<String> = None()
let result = optional_value ?? "default" // Uses default if None
// Safe navigation operator (avoids null dereference)
let maybe_person: Optional<Person> = None()
let name = maybe_person?.name ?? "Unknown" // Safe access, returns "Unknown" if person is None
// Early return operator, return if None(), unwrap if Some()
let definitly_a_person = maybe_person?
Precedence
Operators have a precedence that determines the order in which they are evaluated.
Operators sorted by precedence, from highest to lowest:
| Prec. | Operator |
|---|---|
| 0 | () |
| 1 | [] . ?. !! ? ?? |
| 2 | ! - + (unary) and as is !is in !in |
| 3 | * / % |
| 4 | + - |
| 5 | ..= ..< |
| 6 | << >> >>> |
| 7 | & |
| 8 | ^ |
| 9 | | |
| 10 | < > <= >= |
| 11 | <=> |
| 12 | == != |
| 13 | ^^ |
| 14 | && |
| 15 | || |
| 16 | = += -= *= /= %= |
- Precedence 0 (highest): Will evaluate first
- Precedence 16 (lowest): Will be the last to evaluate
- Operators in the same precedence level are evaluated left-to-right (left-associative)
- Some operators cannot chain, such as
+=and-=, which require a single left-hand operand
Note: This precedence is subject to change in the future
String Interpolation
Nitro supports powerful string interpolation for embedding values and expressions directly in strings:
Basic Interpolation
let name = "Alice"
let age = 42
// Simple variable interpolation
let greeting = "Hello, $name!"
// Expression interpolation
let message = "You are $age years old"
let summary = "In 10 years, you'll be ${age + 10}"
Complex Expressions
Use ${} for complex expressions:
let numbers = [1, 2, 3, 4, 5]
let info = "The list has ${numbers.len} elements"
// Method calls
let text = "HELLO"
let formatted = "Lowercase: ${text.to_lowercase()}"
// Nested interpolation
let nested = "Result: ${"Value is ${42 * 2}"}"
Escape Sequences
let escaped = "Line one\nLine two\tTabbed"
let quote = "She said \"Hello!\""
let backslash = "Path: C:\\folder\\file.txt"
let dolar = "Price: \$100"
Ranges
Ranges represent a sequence of contiguous values efficiently by storing only the start and end values:
Creating Ranges
// Inclusive range (includes both endpoints)
let inclusive = 1..=10 // 1, 2, 3, ..., 9, 10
// Exclusive range (excludes the end)
let exclusive = 1..<10 // 1, 2, 3, ..., 8, 9
Note: The operators
..=and..<can be implemented for any custom type by implementing magic functions.
Using Ranges
let range = 1..=10
// Check membership
println(5 in range) // true
println(15 in range) // false
println(10 in 1..<10) // false (exclusive end)
// Iterate over ranges (in loops)
for i in 1..=5 {
println(i) // Prints 1, 2, 3, 4, 5
}
The Nothing Type
The nothing literal represents the absence of a value, similar to void or Unit in other languages:
// Functions that don't return a value
fun print_message() {
println("This function returns nothing")
} // Implicitly returns: nothing
// Explicit nothing return
fun do_something(): Nothing {
println("Doing something...")
return nothing
}
Important: Unlike null in other languages, nothing is a distinct type that cannot be mixed with other types.
It is used to indicate that a function or expression intentionally does not produce a meaningful value.
The Never Type
The never type represents a situation that will never occur. It is used to make functions that will never return, for
example, with an infinite loop or a critical error that will crash the program:
// Function that never returns
fun infinite_loop(): Never {
loop {
// Do something forever
}
}
// Function that always crashes
fun print_and_exit(msg: String): Never {
println(msg)
runtime::exit(1)
}
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.
| Operator | Function Name | Parameters | Return Type |
|---|---|---|---|
+ | plus | AnyType, AnyType | AnyType |
- | minus | AnyType, AnyType | AnyType |
* | mul | AnyType, AnyType | AnyType |
/ | div | AnyType, AnyType | AnyType |
% | rem | AnyType, AnyType | AnyType |
<=> | get_ordering | AnyType, AnyType | Ordering |
== | is_equals | AnyType, AnyType | Boolean |
^^ | logical_xor | AnyType, AnyType | Boolean |
&& | logical_and | AnyType, AnyType | Boolean |
|| | logical_or | AnyType, AnyType | Boolean |
! | logical_not | AnyType | Boolean |
[] | get | AnyType, Int | AnyType |
[]!! | unsafe_get | AnyType, Int | AnyType |
a[i] = | set | AnyType, Int, AnyType | Nothing |
a[] = | add | AnyType, AnyType | Nothing |
..< | range_up_to | AnyType, AnyType | AnyType |
..= | range_to | AnyType, AnyType | AnyType |
<< | bitwise_shift_left | AnyType, Int | AnyType |
>> | bitwise_shift_right | AnyType, Int | AnyType |
>>> | bitwise_shift_right_unsigned | AnyType, Int | AnyType |
& | bitwise_and | AnyType, AnyType | AnyType |
^ | bitwise_xor | AnyType, AnyType | AnyType |
| | bitwise_or | AnyType, AnyType | AnyType |
? | is_returnable_error | AnyType | Boolean |
!! | get_or_crash | AnyType | AnyType |
| for loop | to_iterator | AnyType | AnyType |
| for loop | next_item | AnyType | Optional<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!"
Collections
Nitro provides several built-in collection types for storing and organizing data. Each collection type is optimized for different use cases and access patterns.
Lists
Lists are ordered, mutable collections that can grow and shrink dynamically.
Creating Lists
// Empty list
let empty_list = List::new<Int>()
// List with initial values
let numbers = [1, 2, 3, 4, 5]
let fruits = ["apple", "banana", "cherry"]
// Mixed types not allowed
let mixed = [1, "hello"] // Compile error: Mismatched types Int and String
Accessing Elements
let numbers = [10, 20, 30, 40, 50]
// Safe indexing (returns Optional)
let first: Optional<Int> = numbers[0] // Some(10)
let invalid: Optional<Int> = numbers[10] // None
// Unsafe indexing (crashes if out of bounds)
let first_value = numbers[0]!! // 10
let last_value = numbers[numbers.len - 1]!! // 50
// Convenience methods
let first_safe = numbers.first() // Optional<Int>
let last_safe = numbers.last() // Optional<Int>
Modifying Lists
let my_list = [1, 2, 3]
// Add elements
my_list[] = 4 // Append: [1, 2, 3, 4]
my_list.add(5) // Same as above: [1, 2, 3, 4, 5]
my_list.insert(2, 0) // Insert at index: [1, 2, 0, 3, 4, 5]
// Modify elements
my_list[1] = 10 // [0, 10, 2, 3, 4, 5]
// Remove elements
let removed = my_list.remove_at(1)!! // Removes and returns 10
let last = my_list.remove_last()!! // Removes and returns last element (5)
let first = my_list.remove_first()!! // Removes and returns first element (0)
my_list.clear() // Empty list: []
List Operations
let numbers = [1, 2, 3, 4, 5]
// Check membership
let contains_three = 3 in numbers // true
let not_contains_ten = 10 !in numbers // true
// Size and properties
let size = numbers.len // 5
let is_empty = numbers.is_empty() // false
let opposite = numbers.is_not_empty() // true
let first_index = numbers.index_of(3) // Some(2) (returns index of first occurrence)
let last_index = numbers.last_index_of(4) // Some(3) (returns index of last occurrence)
// Iteration
for number in numbers {
println(number)
}
Maps
Maps store key-value pairs, providing fast lookups by key. All keys must be the same type, and all values must be the same type.
Creating Maps
// Empty map
let empty_map = Map::new<String, Int>()
// Map with initial values
let person = #[name: "John", surname: "Smith"]
let scores = #["alice": 95, "bob": 87, "charlie": 92]
// Type annotations when needed
let config: Map<String, Int> = #["timeout": 30, "retries": 3]
Accessing Values
let person = #[name: "John", age: "30", city: "New York"]
// Safe access (returns Optional)
let name: Optional<String> = person["name"] // Some("John")
let country: Optional<String> = person["country"] // None
// Unsafe access (crashes if key doesn't exist)
let name_direct = person["name"]!! // "John"
// Alternative access methods
let has_age = "age" in person // true
Modifying Maps
let person = #[name: "John", age: "30"]
// Add or update values
person["city"] = "Boston" // Add new key-value pair
person["age"] = "31" // Update existing value
person[] = Pair::of("country", "USA") // Alternative way
// Remove values
let removed_age = person.remove("age") // Returns Optional<String>
person.clear() // Remove all entries
Map Operations
let scores = #["alice": 95, "bob": 87, "charlie": 92]
// Size and properties
let size = scores.len // 3
let is_empty = scores.is_empty() // false
let is_not_empty = scores.is_not_empty() // true
// Get all keys or values
let players = scores.keys_to_list() // List of keys
let all_scores = scores.values_to_list() // List of values
let entries = scores.to_list() // List of key-value pairs
// Check membership
let has_alice = "alice" in scores // true
let no_david = "david" !in scores // true
Sets
Sets store unique elements with no duplicates. They're ideal for membership testing and eliminating duplicates.
Creating Sets
// Empty set
let empty_set = Set::new<Int>()
// Set with initial values
let fruits = %["apple", "banana", "cherry"]
let numbers = %[1, 2, 3, 4, 5]
// Type annotation when needed
let unique_ids: Set<Int> = %[101, 102, 103]
Set Operations
let fruits = %["apple", "banana"]
// Add elements (duplicates ignored)
fruits[] = "cherry" // Add "cherry"
fruits[] = "apple" // Ignored (already exists)
fruits.add("date") // Desugared form
// Check size
let count = fruits.len // Number of unique elements
// Check membership (very fast)
let has_apple = "apple" in fruits // true
let no_grape = "grape" !in fruits // true
// Remove elements
let removed = fruits.remove("banana") // Returns bool
fruits.clear() // Remove all elements
// Convert to list
let fruit_list = fruits.to_list() // List of unique elements
Set Mathematics
let set_a = %[1, 2, 3, 4]
let set_b = %[3, 4, 5, 6]
// Union (all elements from both sets)
let union = set_a.union(set_b) // %[1, 2, 3, 4, 5, 6]
// Intersection (common elements)
let intersection = set_a.intersection(set_b) // %[3, 4]
// Difference (elements in A but not in B)
let difference = set_a.difference(set_b) // %[1, 2]
Other Collections
Pair
Pairs store exactly two values, useful for returning multiple values or creating simple associations:
// Creating pairs
let coordinates = Pair::of(10, 20)
let name_age = Pair::of("Alice", 25)
// Accessing elements
let x = coordinates.first // 10
let y = coordinates.second // 20
// Functions returning pairs
fun get_both_ends(numbers: List<Int>): Pair<Int, Int> {
let first = numbers.first()!!
let last = numbers.last()!!
return Pair::of(first, last)
}
ArrayDeque
ArrayDeque is a resizable array implementation of a double-ended queue (deque) that provides efficient insertion and removal operations at both the front and back of the collection. This versatility allows ArrayDeque to function as both a Queue (FIFO - first in, first out) and a Stack (LIFO - last in, first out) data structure.
let deque = ArrayDeque::new()
// Add to both ends
deque.add_first(2) // [2]
deque.add_first(1) // [1, 2]
deque.add_last(3) // [1, 2, 3]
deque.add_last(4) // [1, 2, 3, 4]
// Remove from both ends
let first = deque.remove_first()!! // 1, deque = [2, 3, 4]
let last = deque.remove_last()!! // 4, deque = [2, 3]
JSON Collections
Nitro provides built-in JSON support for working with dynamic data:
// JSON objects and arrays
let user_data: Json = json! {"name": "Alice", "age": 42, "active": true}
let shopping_list: Json = json! ["apple", "banana", "cherry"]
// Enhanced JSON syntax
let computed_data = json! {
name: "Bob", // Quotes optional for keys
age: (30 + 5), // Expressions in parentheses
birth_year: (2024 - 35),
hobbies: ["reading", "coding"],
}
// Accessing JSON values
let name = user_data["name"]?.as_string()!!
let age = user_data["age"]?.as_int()!!
Collection Conversion
// Converting between collection types
let list = [1, 2, 3, 2, 1]
let unique_set = list.to_set() // %[1, 2, 3]
let back_to_list = unique_set.to_list() // [1, 2, 3] (order may vary)
// List to map (with transformation)
let names = ["alice", "bob", "charlie"]
let name_lengths = names.map @{ name -> Pair::of(name, name.len) }.to_map()
// Result: #["alice": 5, "bob": 3, "charlie": 7]
Other Collection Types
Check the standard library at src/main/nitro/core/collections where all the collection types are defined.
Types
Nitro's type system provides powerful tools for creating custom types, handling errors safely, and writing generic code. This chapter covers structs, options, enums, and the trait system.
Structs
Structs group related data together into custom types. They're similar to classes in object-oriented languages but focus purely on data storage.
Defining Structs
// Basic struct definition
struct Book {
title: String
author: String
isbn: String
pages: Int
published_year: Int
}
// Comas are optional
struct Point3D {
x: Float,
y: Float,
z: Float,
}
// Struct with optional fields
struct User {
name: String
email: String
age: Optional<Int> // Age might not be provided
profile_picture: Optional<String>
}
Creating and Using Structs
// Create struct instances
let book = Book @[
title: "The Rust Programming Language",
author: "Steve Klabnik",
isbn: "978-1-59327-828-1",
pages: 560,
published_year: 2018,
]
let origin = Point3D @[x: 0.0, y: 0.0, z: 0.0]
// Access fields
let book_title = book.title // "The Rust Programming Language"
let page_count = book.pages // 560
let coordinates = "(${origin.x}, ${origin.y}, ${origin.z})"
// Modify fields (if struct is mutable)
let user = User @[
name: "Alice",
email: "alice@example.com",
age: Some(25),
profile_picture: None(),
]
user.age = Some(26) // Update age
Struct Methods
You can define methods for structs, the same way that any other type:
// Method syntax
fun Book.is_long(): Boolean {
return this.pages > 300
}
// 100% the same as:
fun is_long(book: Book): Boolean {
return book.pages > 300
}
// You can omit the `this` keyword and access fields directly
fun Point3D.distance_from_origin(): Float {
return sqrt(x * x + y * y + z * z)
}
// Using methods
let is_thick_book = book.is_long() // true (560 > 300)
let distance = origin.distance_from_origin() // 0.0
Methods and functions are interchangeable in Nitro, they are only a syntactic difference.
You can use the syntax that is more convenient in each case, for example, max(a, b) and book.is_long() are more
readable than a.max(b) and is_long(book), is up to your preference.
Options and Enums
Option Types
Option types represent values that may have multiple variants, with each variant potentially carrying different data.
// Custom option type
option IceCreamChoice {
Yes { flavor: String, scoops: Int }
No { reason: String }
Maybe { deadline: String }
}
// Creating option instances
let choice1 = IceCreamChoice::Yes @[flavor: "chocolate", scoops: 2]
let choice2 = IceCreamChoice::No @[reason: "I'm on a diet"]
let choice3 = IceCreamChoice::Maybe @[deadline: "after dinner"]
Built-in Option Types
Nitro provides two essential option types for common patterns:
Optional Type
Represents values that may or may not exist:
// Long form
let has_value = Optional::Some @[value: 42]
let no_value = Optional::None @[]
// Convenient shorthand
let has_value = Some(42)
let no_value = None()
// Working with Optional values
fun find_user(id: Int): Optional<User> {
// Search logic here...
return if user_exists(id) {
Some(load_user(id))
} else {
None()
}
}
// Safe access
let user = find_user(123)
let user_name = user?.name ?? "Unknown User" // Safe navigation, returns "Unknown User" if None
// Unsafe access (crashes if None)
let user_name = find_user(123)!!.name
Result Type
Represents operations that can succeed or fail:
// Long form
let success = Result::Ok @[value: "Operation completed"]
let failure = Result::Err @[value: "Network timeout"]
// Convenient shorthand
let success = Ok("Data loaded successfully")
let failure = Err("File not found")
// Functions returning Results
fun divide(a: Float, b: Float): Result<Float, String> {
if b == 0.0 {
return Err("Division by zero")
} else {
return Ok(a / b)
}
}
// Error handling
let result = divide(10.0, 2.0)
let value = result? // '?' will give the value if Ok, or return the function with the Err() variant, propagating the error
Enums
Enums define a fixed set of named constants, optionally with associated data:
Simple Enums
// Basic enum without data
enum Direction {
North
South
East
West
}
// Using enum values
let current_direction = Direction::North
let opposite = Direction::South
Enums with Data
// Enum with associated data
enum Direction {
let x: Int
let y: Int
North @[x: 0, y: 1]
South @[x: 0, y: -1]
East @[x: 1, y: 0]
West @[x: -1, y: 0]
}
// Accessing enum data
let north = Direction::North
let north_x = Direction::North.x // 0
let north_y = Direction::North.y // 1
// Pattern matching with enums
fun move_character(direction: Direction, distance: Int): Point {
return match direction {
Direction::North -> Point @[x: 0, y: distance]
Direction::South -> Point @[x: 0, y: -distance]
Direction::East -> Point @[x: distance, y: 0]
Direction::West -> Point @[x: -distance, y: 0]
}
}
Enum Utilities
Enums automatically provide useful methods:
// Get all enum values
let all_directions: List<Direction> = Direction::values()
// Convert from string name
let north_from_name = Direction::from_name("North") // Optional<Direction>
// Convert from index
let north_from_index = Direction::from_variant(0) // Optional<Direction>
// Get variant index
let north_index = Direction::North.variant // 0
// Iterate over all values
for direction in Direction::values() {
// to_string() is automatically generated
println("Direction: ${direction}, Vector: (${direction.x}, ${direction.y})")
}
Tags
Tags (similar to traits in other languages) define a set of methods that types must implement. They enable polymorphism and code reuse through shared behavior.
The main difference in Nitro is that tags are automatically assigned to types that implement the required methods, instead of explicitly declaring that a type implements a tag.
Defining Tags
// Define a tag for types that can be printed
// `This` refers to the type that implements the tag
tag Printable {
fun This.print() // Method that implementers must provide
fun This.print_debug() // Another required method
}
// Define a tag for types that can be compared
tag Comparable {
fun This.get_ordering(other: This): Ordering
}
// Define a tag with default implementations
tag Drawable {
fun This.draw() // Required method
}
// This acts as a default implementation, since any Drawable type will have this function available, and any override will have higher priority
fun <#T: Drawable> #T.draw_with_color(color: String) {
println("Drawing with color: $color")
this.draw()
}
Implementing Tags
Types automatically implement tags when they have the required methods:
// Implement Printable for Int
fun Int.print() {
println("Int value: $this")
}
fun Int.print_debug() {
println("Debug - Int: $this (size: ${size_of<Int>()})")
}
// Implement Printable for String
fun String.print() {
println("String: '$this'")
}
fun String.print_debug() {
println("Debug - String: '$this' (length: ${this.len})")
}
// Now Int and String automatically implement Printable
Using Tags
Generic Functions with Tag Constraints
// Function that works with any Printable type
fun <#T: Printable> print_and_debug(value: #T) {
value.print()
value.print_debug()
}
// Function with multiple tag constraints
fun <#T: Printable & Comparable> process_item(item: #T) {
// Can also use comparison methods
if item > 10 {
item.print()
}
}
// Using the generic functions
fun main() {
print_and_debug(42) // Works with Int
print_and_debug("Hello") // Works with String
let numbers = [3, 1, 4, 1, 5]
for num in numbers {
process_item(num)
}
}
Built-in Tags
Nitro provides several built-in tags:
- GetOrdering
- ToString
- ToBoolean
You can add tags to existing types and apply tags to new types you define, there is no limit. This allows easier interoperability with libraries, using library functions with your types and using your functions with library types.
GetOrdering Example
// Provides comparison between values of the same type
tag GetOrdering {
fun This.get_ordering(other: This): Ordering
}
struct MyType { ... }
// Implementing GetOrdering for MyType
fun MyType.get_ordering(other: MyType): Ordering { ... }
fun main() {
let a = MyType @[ ... ]
let b = MyType @[ ... ]
when {
a < b -> println("a is less than b")
a > b -> println("a is greater than b")
else -> println("a is equal to b")
}
}