Optionals

Optional types allow you to express the possibility that a value might be absent. This is safer than using null pointers because the type system forces you to explicitly handle the missing case.

Declaring Optional Types

You can make any type in Conjure optional by prefixing it with ?. An optional type can hold either a value of its underlying type or a nothing value represented by null.

var age ?i32 = 25
var name ?string = "Alice"
var missing ?f32 = null

The Null Value

The null keyword represents the absence of a value. It can be assigned to any optional type. When used in the context of a pointer, null indicates that the pointer does not point to any valid memory and becomes equivalent to C’s NULL.

var count ?i32   = null
var text ?string = null

Non-optional types cannot be null. This makes it clear from the type signature whether a value might be missing.

var x i32  = null  // Error: i32 is not optional
var y ?i32 = null  // OK: ?i32 can be null

Narrowing Optionals

We can use comparison operators to check if an optional contains a value. When used in a boolean context, an optional evaluates to true if it contains a value and false if it is null.

var value ?i32 = 42

// The following is equivalent to `value != null`
if value {
    c.printf("Value exists\n")
}

// The following is equivalent to `value == null`
if !value {
    c.printf("Value is missing\n")
}

When checking for null, the compiler can automatically narrow the type within the conditional block. This means you can safely access the underlying value without additional unwrapping.

var value ?i32 = 42

if value != null {
    var x = value  // x is now i32, not ?i32
    c.printf("Value is %d\n", x)
}

Default Values

Provide a default value using the else operator when unwrapping.

var userAge ?i32 = getAge()

var age = userAge else 0

c.printf("Age: %d\n", age)

If userAge is null, age receives the default value of 0. Otherwise, it receives the unwrapped value.

Pattern Matching with Optionals Proposed

The when expression provides elegant optional handling.

var result ?string = findUser("alice")

when result {
    null: c.printf("User not found\n")
    else: c.printf("Found: %s\n", result)
}

Optional Chaining Proposed

When working with nested optionals or optional struct fields, optional chaining prevents verbose null checks.

struct User {
    name string
    email ?string
}

var user ?User = findUser("alice")

// Without optional chaining
var email = null
if user != null {
    email = user.email
}

// With optional chaining
var email = user?.email

The ?. operator short-circuits if any value in the chain is null, returning null for the entire expression.

struct Profile {
    user ?User
}

struct User {
    settings ?Settings
}

struct Settings {
    theme string
}

var profile ?Profile = loadProfile()
var theme = profile?.user?.settings?.theme else "dark"

Optional Function Parameters

Functions can accept optional parameters, making arguments truly optional rather than requiring empty default values.

func greet(name ?string) {
    var displayName = name else "Guest"
    c.printf("Hello, %s!\n", displayName)
}

greet("Alice")
greet(null)

Returning Optionals

Functions often return optionals to indicate that an operation might not produce a result. This is clearer than returning -1 or other magic values to indicate failure.

func findElement(arr []i32, target i32) ?i32 {
    for i, value in arr {
        if value == target {
            return i
        }
    }
    return null
}

var index = findElement(numbers, 42)
if index != null {
    c.printf("Found at index %d\n", index)
}

Optional Pointers

Both pointers and optionals represent potential absence, but they’re different concepts. Regular pointers in Conjure are nullable by default. Optional types are for value semantics where you want to explicitly handle absence.

var ptr *i32 = null        // nullable pointer
var opt ?i32 = null        // optional value
var optPtr ?*i32 = null    // optional pointer

Converting Between Optionals and Non-Optionals

You can always convert a non-optional to an optional implicitly.

var x i32 = 42
var y ?i32 = x  // implicit conversion

Converting from optional to non-optional requires explicit handling of the null case.

var x ?i32 = 42
var y i32 = x else 0  // explicit default

Optional Arrays

Optional types work with any type, including arrays.

var numbers ?[]i32 = null
var items ?[5]i32 = null

if numbers != null {
    for num in numbers {
        c.printf("%d ", num)
    }
}

Common Patterns

Optional types are commonly used for configuration values, search results, and optional struct fields.

struct Config {
    host string
    port ?i32  // port is optional, has default
    debug ?bool
}

func loadConfig() Config {
    return Config{
        host = "localhost",
        port = null,  // use default
        debug = true,
    }
}

var config = loadConfig()
var port = config.port else 8080

Optionals vs Error Types

Optional types indicate absence, while error types indicate failure. Use optionals when something simply doesn’t exist, and errors when an operation failed.

// Use optional: value might not exist
func findUser(id i32) ?User

// Use error: operation might fail
func loadUser(id i32) error[User]

Sometimes you need both to distinguish between “not found” and “lookup failed”.

func findUser(id i32) error[?User] {
    var result = try databaseQuery(id)
    if result.isEmpty() {
        return null  // not found, but no error
    }
    return result.user
}

Optional Struct Fields

Struct fields can be optional, making certain fields explicitly optional without requiring pointer indirection. This is more type-safe than using empty strings or zero values to represent absence.

struct Person {
    name string
    age i32
    email ?string  // not everyone has an email
    phone ?string  // not everyone has a phone
}

var person = Person{
    name = "Bob",
    age = 30,
    email = "bob@example.com",
    phone = null,
}