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..}.