Jump to page: 1 2 3
Thread overview
'unwrap envy' and exceptions
Sep 10, 2021
jfondren
Sep 10, 2021
Dom DiSc
Sep 10, 2021
Timon Gehr
Sep 10, 2021
jfondren
Sep 10, 2021
Kagamin
Sep 10, 2021
jfondren
Sep 10, 2021
Paul Backus
Sep 10, 2021
jfondren
Sep 10, 2021
Paul Backus
Sep 10, 2021
James Blachly
Sep 10, 2021
IGotD-
Sep 11, 2021
Mike Parker
Sep 11, 2021
Dukc
Sep 12, 2021
H. S. Teoh
Sep 12, 2021
jfondren
Sep 12, 2021
Paulo Pinto
Sep 12, 2021
IGotD-
Sep 12, 2021
rikki cattermole
Sep 12, 2021
Alexandru Ermicioi
Sep 13, 2021
Paulo Pinto
Sep 15, 2021
Alexandru Ermicioi
Sep 13, 2021
Paulo Pinto
Sep 13, 2021
IGotD-
September 10, 2021

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:

  1. if nums is empty, the program halts with a (normally) uncatchable error.
  2. those verbose unwraps 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:

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:

  1. if nums is empty, the program halts with a (normally) uncatchable error.
  2. those verbose unwraps 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:

// 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?

September 10, 2021

On Friday, 10 September 2021 at 02:57:37 UTC, jfondren wrote:

>
assert(find_gcd([3, 5, 12, 15]) == 3);

Maybe a little off topic, but since when is gcd(3,5) != 1 ??
You're program seems to have some bugs...

September 10, 2021
On 10.09.21 10:59, Dom DiSc wrote:
> On Friday, 10 September 2021 at 02:57:37 UTC, jfondren wrote:
>>     assert(find_gcd([3, 5, 12, 15]) == 3);
> 
> Maybe a little off topic, but since when is gcd(3,5) != 1 ??
> You're program seems to have some bugs...

It's not computing the gcd of the list, just the gcd of its minimum and maximum.
September 10, 2021
On Friday, 10 September 2021 at 10:17:12 UTC, Timon Gehr wrote:
> On 10.09.21 10:59, Dom DiSc wrote:
>> On Friday, 10 September 2021 at 02:57:37 UTC, jfondren wrote:
>>>     assert(find_gcd([3, 5, 12, 15]) == 3);
>> 
>> Maybe a little off topic, but since when is gcd(3,5) != 1 ??
>> You're program seems to have some bugs...
>
> It's not computing the gcd of the list, just the gcd of its minimum and maximum.

yep, it's a leetcode problem and this min/max requirement is probably an arbitrary confounder to make for more interesting code than gcd(nums): https://leetcode.com/problems/find-greatest-common-divisor-of-array/
September 10, 2021
Nullable doesn't replace exceptions, it specifies a value which is legitimately absent, not due to an error, it's `Result` that replaces exceptions. Also languages built with this pattern in mind have syntax sugar that helps propagate those errors upwards.
September 10, 2021

On Friday, 10 September 2021 at 15:26:18 UTC, Kagamin wrote:

>

Nullable doesn't replace exceptions, it specifies a value which is legitimately absent, not due to an error

Asking for the maximum of an empty array is an error case (hence D's minElement/maxElement thrown an AssertError, not even a catchable exception), this is just a boring and expected one, and there aren't any other kinds of errors that need to be distinguished from it, so it's reasonable to return Option instead of Result. Everything works the same for them, Result just has more runtime and programmer overhead. Hence also

> >

Conclusion: deprecate exceptions, rewrite Phobos to only use Option and Result sumtypes, and release D3!

rather than "deprecate exceptions, rewrite Phobos to use Nullable".

>

, it's Result that
replaces exceptions. Also languages built with this pattern in mind have syntax sugar that helps propagate those errors upwards.

Yeah, .assumeWontThrow in a nothrow function is very similar to .unwrap, but there's no analogue to ? in a function that just propagates errors. Temporarily adding nothrow still tells you where exceptions might be coming from, and I suppose a Sufficiently Smart IDE could toggle that behind your back to annotate uncaught exceptional function calls for you.

September 10, 2021

On Friday, 10 September 2021 at 15:44:04 UTC, jfondren wrote:

>

On Friday, 10 September 2021 at 15:26:18 UTC, Kagamin wrote:

>

Nullable doesn't replace exceptions, it specifies a value which is legitimately absent, not due to an error

Asking for the maximum of an empty array is an error case (hence D's minElement/maxElement thrown an AssertError, not even a catchable exception)

Strangely, there is no mention of this in the documentation for either minElement or maxElement. And it's also inconsistent with other similar functions in std.algorithm.searching; for example, minCount and maxCount throw an Exception on an empty range.

September 10, 2021

On Friday, 10 September 2021 at 15:57:03 UTC, Paul Backus wrote:

>

On Friday, 10 September 2021 at 15:44:04 UTC, jfondren wrote:

>

On Friday, 10 September 2021 at 15:26:18 UTC, Kagamin wrote:

>

Nullable doesn't replace exceptions, it specifies a value which is legitimately absent, not due to an error

Asking for the maximum of an empty array is an error case (hence D's minElement/maxElement thrown an AssertError, not even a catchable exception)

Strangely, there is no mention of this in the documentation for either minElement or maxElement. And it's also inconsistent with other similar functions in std.algorithm.searching; for example, minCount and maxCount throw an Exception on an empty range.

In a -release build, over an int[], you get a RangeError from std.range.primitives.front:

    assert(a.length, "Attempting to fetch the front of an empty array of " ~ T.stringof);
    return a[0];

So the skipped assert() there and the skipped contract programming assert() in minElement/maxElement, all they're really doing is improving the error message. That might be why they're not mentioned.

... or in a -release -boundscheck=off build, you get Error: program killed by signal 11. These kind of flags also weaken the assumeWontThrow=unwrap similarity.

September 10, 2021

On Friday, 10 September 2021 at 16:10:27 UTC, jfondren wrote:

>

In a -release build, over an int[], you get a RangeError from std.range.primitives.front:

    assert(a.length, "Attempting to fetch the front of an empty array of " ~ T.stringof);
    return a[0];

So the skipped assert() there and the skipped contract programming assert() in minElement/maxElement, all they're really doing is improving the error message. That might be why they're not mentioned.

Either way, if a function has a precondition that the caller needs to satisfy, it should be documented.

https://github.com/dlang/phobos/pull/8238

September 10, 2021
On 9/10/21 11:44 AM, jfondren wrote:
> Yeah, .assumeWontThrow in a nothrow function is very similar to .unwrap, but there's no analogue to ? in a function that just propagates errors. Temporarily adding nothrow still tells you where exceptions might be coming from, and I suppose a Sufficiently Smart IDE could toggle that behind your back to annotate uncaught exceptional function calls for you.

I've really come to appreciate `?` operator, and don't forget there is a complement of other library infrastructure like `map_err`, `unwrap_or_else`, etc.

« First   ‹ Prev
1 2 3