'unwrap envy' and exceptions
jfondren
julian.fondren at gmail.com
Fri Sep 10 02:57:37 UTC 2021
This programming chrestomathy video:
https://www.youtube.com/watch?v=UVUjnzpQKUo
has this Rust code:
```rust
fn find_gcd(nums: Vec<i32>) -> i32 {
num::integer::gcd(*nums.iter().max().unwrap(),
*nums.iter().min().unwrap())
}
```
Here, both `max()` and `min()` return an `Option<i32>`--a sumtype
over `Some(i32)` in the case that `nums` has any numbers in it,
and `None` in the case that it's empty. They do this rather than
throw an exception or otherwise signal the error.
`find_gcd` could similary return a `None` given an empty `nums`,
but here the programmer has decided that this case should be
treated as an unrecoverable internal error that halts the
program. Hence the two `unwrap()`s in this code: they either pull
the `i32` out of a `Some(i32)` or they halt the program.
Here's similar code in D, using std.typecons.Nullable and
implementing our own min/max with optional results:
```d
import std.typecons : Option = Nullable, some = nullable;
alias unwrap = (x) => x.get;
Option!int most(string op)(const int[] nums) {
if (nums.length) {
int n = nums[0];
foreach (m; nums[1 .. $]) {
mixin("if (m " ~ op ~ " n) n = m;");
}
return some(n);
} else {
return typeof(return).init;
}
}
alias min = most!"<";
alias max = most!">";
int find_gcd(const int[] nums) {
import std.numeric : gcd;
return gcd(nums.min.unwrap, nums.max.unwrap);
}
unittest {
import std.exception : assertThrown;
import core.exception : AssertError;
assert(find_gcd([3, 5, 12, 15]) == 3);
assertThrown!AssertError(find_gcd([]));
}
```
That `find_gcd` isn't too bad, is it? Now that we've seen it we
can forget about the Rust. I'm not going to mention Rust again.
Let's talk about how nice this `find_gcd` is:
1. if nums is empty, the program halts with a (normally)
uncatchable error.
2. those verbose `unwrap`s clearly tell us where the program is
prepared to halt with an error, and by their absence where it
isn't.
3. because `Option!int` and `int` are distinct types that don't
play well together, we get clear messages from the compiler if we
forget to handle min/max's error case
4. because `Option!T` is a distinct type it can have its own
useful methods that abstract over error handling, like `T
unwrap_or(T)(Option!T opt, T alternate) { }` that returns the
alternate in the None case.
5. since exceptions aren't being used, this can avoid paying the
runtime costs of exceptions, can be nothrow, can be used by
BetterC, can more readily be exposed in a C ABI for other
languages to use, etc.
The clear messages in the third case:
```d
int broken_find_gcd(const int[] nums) {
import std.numeric : gcd;
return gcd(nums.min, nums.max);
// Error: template `std.numeric.gcd` cannot deduce function
from argument types `!()(Option!int, Option!int)`, candidates
are: ...
}
```
Conclusion: deprecate exceptions, rewrite Phobos to only use
Option and Result sumtypes, and release D3!
.
.
.
Please consider this code:
```d
import std.exception;
int most(string op)(const int[] nums) {
if (nums.length) {
int n = nums[0];
foreach (m; nums[1 .. $]) {
mixin("if (m " ~ op ~ " n) n = m;");
}
return n;
} else {
throw new Exception("not an AssertError");
}
}
alias min = most!"<";
alias max = most!">";
int find_gcd(const int[] nums) nothrow {
import std.numeric : gcd;
return gcd(nums.min.assumeWontThrow,
nums.max.assumeWontThrow);
}
unittest {
import std.exception : assertThrown;
import core.exception : AssertError;
assert(find_gcd([3, 5, 12, 15]) == 3);
assertThrown!AssertError(find_gcd([]));
}
```
Or with the obvious alias:
```d
int find_gcd(const int[] nums) nothrow {
import std.numeric : gcd;
return gcd(nums.min.unwrap, nums.max.unwrap);
}
```
Things that can be said about this code:
1. if nums is empty, the program halts with a (normally)
uncatchable error.
2. those verbose `unwrap`s clearly tell us where the program is
prepared to halt with an error, and by their absence where it
isn't.
3. because min/max otherwise throws, and because the function is
nothrow, we get clear messages from the compiler if we forget to
handle these error cases.
4. because D is so expressive, we can have useful abstractions
over error handling like std.exception.ifThrown, where we can
provide an alternate that's used in the error case.
5. since exceptions aren't leaving this function but cause the
program to halt, we can (theoretically, with a Sufficiently Smart
Compiler) avoid paying the runtime costs of exceptions, can be
nothrow, can (theoretically) be used by BetterC, can more readily
be exposed in a C ABI for other languages to use, etc.
The clear messages in the third case:
```d
// Error: `nothrow` function `exceptions2.broken_find_gcd` may
throw
int broken_find_gcd(const int[] nums) nothrow {
import std.numeric : gcd;
return gcd(nums.min, nums.max);
// Error: function `exceptions2.most!"<".most` is not
`nothrow`
}
```
That's a very similar list of features. That's some very dubious
handwaving about the potential performance benefits where the
compiler magically replaces normal non-Error exceptions with
program-halts if the only consumer of the exception is an
assumeWontThrow. On the other hand, Phobos doesn't have to be
rewritten. On the gripping hand, I think it's neat that
status-quo D has the other benefits just from slapping a
`nothrow` attribute on a function.
Thoughts?
More information about the Digitalmars-d
mailing list