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 = nullThe 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 = nullNon-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 nullNarrowing 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?.emailThe ?. 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 pointerConverting 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 conversionConverting from optional to non-optional requires explicit handling of the null case.
var x ?i32 = 42
var y i32 = x else 0 // explicit defaultOptional 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 8080Optionals 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,
}