4 ms·
> Optionals are not zero-overhead in any other language. This is a common misconception, and a pet peeve of mine. Properly designed optional types add neither
by emtel 5y ago
> Optionals are not zero-overhead in any other language.
This is a common misconception, and a pet peeve of mine. Properly designed optional types add neither space nor code-size overhead.
Consider the C function:
foo(int* ptr) { ... }
If it is part of the API of foo that `ptr` may be null, then foo must not dereference `ptr` without verifying that it is non-null. So the body of foo must be something like:
foo(int* ptr) {
if (ptr != NULL) {
...
}
}
Or, take the non-pointer case:
foo(struct S s, bool s_present) { ... }
Clearly the intent of this function is that the contents of `s` should not be accessed if `s_present` is false, so the implementation of the function must check `s_present` before inspecting `s`. (Inspecting s incorrectly wouldn't necessarily be UB, like in the previous example, but it's clearly erroneous)
The only thing that a good optional type implementation does is make it a type error to fail to do what everyone agrees both of those functions must do anyway in order to be correct. It need not increase the size of the representations, nor must it emit a single unnecessary instruction when compiled. There are plenty of languages that do this, Rust being a prominent example.
- jcelerier 5y ago> If it is part of the API of foo that `ptr` may be null, then foo must not dereference `ptr` without verifying that it is non-null. So the body of foo must be something like: foo(int* ptr) { if (ptr != NULL) { ... } } then what ? foo(int* ptr) { if (ptr != NULL) { *ptr += 1; // optional<T>::operator* would introduce a branch here ? *ptr = *ptr / 2 ; // same if(*ptr > some_constant) // same { ... } // etc etc } } and compiler optimizations can't always be assumed, for instance debug mode (-O0) performance does matter
- emtel 5y agoThat’s why I said properly implemented! Good optional implementations let you do the check once to unwrap the inner value, so you have one branch. In rust: fn foo(s: Option<&S>) -> { if (let Some(inner) = s) { // now you can access inner repeatedly // without branches } } (edit to fix syntax errors)
- jcelerier 5y agobut this declares a new variable - I find this super messy and really decreases readability in practice since now it's not obvious anymore that what you're working with was a function parameter. Also it adds a scope level - I much prefer the if(!bla) return; // use bla early-return style. So that's really a no-go for me from years comparing the two styles
- emtel 5y agoWell if you have aesthetic objections to the way rust does it, I can't argue with you. Kotlin does it the way you like, though. Anyway, I started in on this thread because I was objecting to the claim that optional types always have overhead. They don't. That's all I wanted to show.
- matvore 5y ago> Well if you have aesthetic objections to the way rust does it, I agree with GP that it is harder to maintain ("messy" they say) if there is more than one variable referring to the same value. It is not so subjective as you make it to be.
- iudqnolq 5y agoThe idiomatic way to solve that in rust is to re-bind to the same variable name. if let Some(foo) = foo { /* ... * / } That's possible because in Rust name shadowing let foo = grab_foo_bytes(); let foo = parse_foo_bytes(foo); makes the previous binding of the variable no longer namable and thus no longer accessible, but doesn't drop it (and trigger RAII destructors). Now someone will probably come in and say "oh no, this isn't exactly like c, how will anyone ever understand it". To that I reply why is it that c users get to say "if you don't know how c works exactly you're holding it wrong" and then comment about other languages "I don't want to have to learn anything to hold it right".
- matvore 5y ago