Maybe Not

Notes are short, informal thought pieces relevant to Radiant Computer.

Rich Hickey, the creator of the Clojure programming language has a great talk called Maybe Not, about how optional values are handled in various languages, eg. Option in Rust and Maybe in Haskell. He argues that using sum types to express optionality isn’t actually what you want.

One example he gives is that you have a function that takes some type T, eg. f(T), and eventually you want to make that T optional, so you change the function signature to f(Option<T>), but this breaks all callers, as they now have to wrap their T in a Some.

// Before.
fn fnord(opt: T) { ... }
...
fnord(t);
// After.
fn fnord(opt: Option<T>) { ... }
...
fnord(Some(t));

This doesn’t quite match the intent of the programmer and can be seen as a design flaw. You’ll also note that this applies for changing an argument from optional to non-optional. What you want is to only get errors on call sites that could omit the value, but in Rust for instance, you will get errors on all call sites.

fn fnord(opt: T) { ... }
...
fnord(Some(t)); // This gives me an error even though I have a `T`.
fnord(None); // But this is actually where the code is logically wrong.

A few languages, including most recently Kotlin and Zig, have taken a different approach that is closer to what Hickey is advocating: in Zig, an optional T is written ?T, and accepts both a T and null. That’s because ?T is the mathematical set {T, null}, not a new type like Option. This gets around the issues mentioned above. Radiance follows the same approach:

// Before. Function requires a `T`.
fn fnord(opt: T) { ... }

fnord(t);
// After. Function takes an optional `T`.
fn fnord(opt: ?T) { ... }
...
fnord(t); // This is still valid!
fnord(nil); // And we can now pass `nil` if we don't have a `T`.

It’s worth noting that this also applies to return values, eg.

fn fnord() -> ?T {
    ...
    return t; // This stays the same whether the function returns `?T` or `T`.
}

The same can be said about fallible functions. Going from T to Result<T, Err> in Rust requires wrapping all success values in Ok. This isn’t the case in Radiance, since the success and error cases form a set {T, Err..}.

Last edited January 02, 2026
Alexis Sellier