Optional (?) types

In Luma, any type can be marked as optional by prefixing it with a question mark (?). An optional type represents a value that may either hold a normal value of that type or be nil.

This makes optional types ideal for representing missing data, conversion failures, or values not yet assigned.

Declaring Optionals

maybeNum: ?int = nil
maybeText: ?str = "hello"
maybePi: ?float = 3.14
flag: ?bool = nil
  • ?int means “either an int or nil”.
  • ?str means “either a str or nil”.
  • Any type can be optional: ?int, ?float, ?str, ?bool, ?byte, ?[byte], and even structs like ?Person.

Assigning Values

maybe: ?int = nil
print(maybe)          // nil

maybe = 42
print(maybe)          // 42

Only optional variables can be assigned nil. Non-optional types always require a value:

count: int = nil    // error

Checking for nil

The most common pattern is to check if a value is present before using it:

maybe: ?str = nil

if maybe == nil {
    print("No value yet")
} else {
    print("We have a value")
}

Comparisons

Optionals can be compared directly to values and to nil. The compiler handles the nil check automatically:

x: ?int = 5
print(x == 5)      // true
print(x > 3)       // true
print(x == nil)     // false

y: ?int = nil
print(y == nil)     // true
print(y == 5)       // false  (nil is never equal to a value)

Two optionals can also be compared:

a: ?int = 5
b: ?int = 5
print(a == b)       // true

c: ?int = nil
d: ?int = nil
print(c == d)       // true  (both nil)

Pattern Matching

Use match to handle the nil and non-nil cases. The binding variable receives the unwrapped value:

result: ?int = "42".to_int()

match result {
    nil -> print("parse failed")
    n -> print(n)        // 42
}

This is the cleanest way to handle optional values — no manual nil checks needed.

Auto-dereferencing

When you assign an optional to a non-optional variable of the same base type, Luma automatically unwraps it:

x: ?int = 5
y: int = x         // y is 5, automatically unwrapped

This is safe when you know the value is not nil. If the value is nil at runtime, it will panic.

Functions With Optionals

Optional return types

Functions can return optionals to indicate that the result may be absent:

fn find(name: str) -> ?int {
    if name == "alice" {
        return 42
    }
    return nil
}

result: ?int = find("alice")    // 42
missing: ?int = find("bob")    // nil

Optional parameters

Functions can accept optional parameters:

fn greet(name: ?str) {
    if name == nil {
        print("Hello, stranger")
    } else {
        print("Hello")
    }
}

Conversion Methods

Many conversion methods return optionals to signal failure safely:

s: str = "123"
n: ?int = s.to_int()     // 123 (as optional)
m: int  = s.to_int(0)    // 123 (with fallback)

bad: ?int = "abc".to_int()   // nil
safe: int = "abc".to_int(0)  // 0

This pattern applies to to_int(), to_float(), to_bool(), to_byte(), and encoding methods like from_hex(). See Type Conversion for details.

Error Recovery With .or()

The .or() method provides a fallback value when a function call panics:

fn safe_div(a: int, b: int) -> int {
    if b == 0 {
        error("division by zero")
    }
    return a / b
}

result: int = safe_div(10, 0).or(0)    // 0 (fallback)
result = safe_div(10, 2).or(0)         // 5 (normal)

.or() can also receive a lambda with the error message:

result: int = safe_div(10, 0).or(err -> {
    print("Error: ${err}")
    return -1
})

Optional Structs

Structs can be optional too:

struct Person {
    name: str
    age: int
}

friend: ?Person = nil
best: ?Person = Person("Alice", 25)

Lists of Optionals

A list can hold optional elements:

scores: [?int] = [1, nil, 3]

String Interpolation

String interpolation with ${} handles optionals correctly, displaying nil for absent values:

x: ?int = 42
y: ?int = nil
print("x is ${x}")   // x is 42
print("y is ${y}")   // y is nil

Quick Reference

Type Example Values Meaning
?int nil, 42 Either no value or an integer
?float nil, 3.14 Either no value or a float
?str nil, "hello" Either no value or a string
?bool nil, true, false Either no value or a boolean
?byte nil, 0x41 Either no value or a single byte
?[byte] nil, [0x48, 0x65] Either no value or a byte list
?Person nil, Person("A", 1) Either no value or a struct

Notes

  • Only optional types can hold nil — assigning nil to a non-optional is a compile error
  • Comparisons like x == 5 on an optional ?int automatically handle the nil case (returns false if nil)
  • match is the recommended way to handle optionals
  • Auto-dereferencing works when assigning ?T to T, but panics if the value is nil at runtime
  • .or() catches panics from error() calls — it’s for error recovery, not nil coalescing
Last updated on