'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