This programming chrestomathy video:
https://www.youtube.com/watch?v=UVUjnzpQKUo
has this Rust code:
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:
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:
- if nums is empty, the program halts with a (normally) uncatchable error.
- 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. - because
Option!int
andint
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 - because
Option!T
is a distinct type it can have its own useful methods that abstract over error handling, likeT unwrap_or(T)(Option!T opt, T alternate) { }
that returns the alternate in the None case. - 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:
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:
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:
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:
- if nums is empty, the program halts with a (normally) uncatchable error.
- 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. - 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.
- 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.
- 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:
// 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?