What's up with Zig's Optionals?
I'm new to this type theory business, so bear with me :) Questions are at the bottom of the post.
I've been trying to learn about how different languages do things, having come from mostly a C background (and more recently, Zig). I just have a few questions about how languages do optionals differently from something like Zig, and what approaches might be best.
Here is the reference for Zig's optionals if you're unfamiliar: https://ziglang.org/documentation/master/#Optionals
From what I've seen, there's sort of two paths for an 'optional' type: a true optional, like Rust's "Some(x) | None", or a "nullable" types, like Java's Nullable. Normally I see the downsides being that optional types can be verbose (needing to write a variant of Some() everywhere), whereas nullable types can't be nested well (nullable nullable x == nullable x). I was surprised to find out in my investigation that Zig appears to kind of solve both of these problems?
A lot of times when talking about the problem of nesting nullable types, a "get" function for a hashmap is brought up, where the "value" of that map is itself nullable. This is what that might look like in Zig:
const std = @import("std");
fn get(x: u32) ??u32 {
if (x == 0) {
return null;
} else if (x == 1) {
return @as(?u32, null);
} else {
return x;
}
}
pub fn main() void {
std.debug.print(
"{?d} {?d} {?d}\n",
.{get(0) orelse 17, get(1) orelse 17, get(2) orelse 17},
);
}
- We return "null" on the value 0. This means the map does not contain a value at key 0.
- We cast "null" to ?u32 on value 1. This means the map does contain a value at key 1; the value null.
- Otherwise, give the normal value.
The output printed is "17 null 2\n". So, we printed the "default" value of 17 on the `??u32` null case, and we printed the null directly in the `?u32` null case. We were able to disambiguate them! And in this case, the some() case is not annotated at all.
Okay, questions about this.
- Does this really "solve" the common problems with nullable types losing information and optional types being verbose, or am I missing something? I suppose the middle case where a cast is necessary is a bit verbose, but for single-layer optionals (the common case), this is never necessary.
- The only downside I can see with this system is that an optional of type `@TypeOf(null)` is disallowed, and will result in a compiler error. In Zig, the type of null is a special type which is rarely directly used, so this doesn't really come up. However, if I understand correctly, because null is the only value that a variable of the type `@TypeOf(null)` can take, this functions essentially like a Unit type, correct? In languages where the unit type is more commonly used (I'm not sure if it even is), could this become a problem?
- Are these any other major downsides you can see with this kind of system besides #2?
- Are there any other languages I'm just not familiar with that already use this system?
Thanks for your help!