www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - 'unwrap envy' and exceptions

reply jfondren <julian.fondren gmail.com> writes:
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?
Sep 09
next sibling parent reply Dom DiSc <dominikus scherkl.de> writes:
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...
Sep 10
parent reply Timon Gehr <timon.gehr gmx.ch> writes:
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.
Sep 10
parent jfondren <julian.fondren gmail.com> writes:
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/
Sep 10
prev sibling parent reply Kagamin <spam here.lot> writes:
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.
Sep 10
next sibling parent reply jfondren <julian.fondren gmail.com> writes:
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.
Sep 10
next sibling parent reply Paul Backus <snarwin gmail.com> writes:
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.
Sep 10
parent reply jfondren <julian.fondren gmail.com> writes:
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: ```d 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.
Sep 10
parent Paul Backus <snarwin gmail.com> writes:
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:

 ```d
     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
Sep 10
prev sibling parent James Blachly <james.blachly gmail.com> writes:
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.
Sep 10
prev sibling parent reply IGotD- <nise nise.com> writes:
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, it's `Result` that 
 replaces exceptions. Also languages built with this pattern in 
 mind have syntax sugar that helps propagate those errors 
 upwards.
Walter mentioned that exceptions are on the way out because they are expensive and inhibits the optimizer which are fair points. I interpreted this that Walter wants to move away from exceptions in D. Since then I haven't seen any proposal for any new error handling for D.
Sep 10
next sibling parent Mike Parker <aldacron gmail.com> writes:
On Friday, 10 September 2021 at 23:20:17 UTC, IGotD- wrote:
ve syntax sugar that helps propagate
 Walter mentioned that exceptions are on the way out because 
 they are expensive and inhibits the optimizer which are fair 
 points. I interpreted this that Walter wants to move away from 
 exceptions in D. Since then I haven't seen any proposal for any 
 new error handling for D.
There has been some brainstorming about it, but don't expect to see a proposal anytime soon.
Sep 10
prev sibling parent reply Dukc <ajieskola gmail.com> writes:
On Friday, 10 September 2021 at 23:20:17 UTC, IGotD- wrote:
 Walter mentioned that exceptions are on the way out because 
 they are expensive and inhibits the optimizer which are fair 
 points. I interpreted this that Walter wants to move away from 
 exceptions in D. Since then I haven't seen any proposal for any 
 new error handling for D.
Well the ` nodiscard` proposal, that is very likely to get accepted, is sort-of one. It's basically about returning traditional error values, but forcing the user to be explicit if she really wants to summarily discard them.
Sep 11
parent reply "H. S. Teoh" <hsteoh quickfur.ath.cx> writes:
On Sat, Sep 11, 2021 at 08:13:07PM +0000, Dukc via Digitalmars-d wrote:
 On Friday, 10 September 2021 at 23:20:17 UTC, IGotD- wrote:
 
 Walter mentioned that exceptions are on the way out because they are
 expensive and inhibits the optimizer which are fair points. I
 interpreted this that Walter wants to move away from exceptions in
 D.  Since then I haven't seen any proposal for any new error
 handling for D.
Well the ` nodiscard` proposal, that is very likely to get accepted, is sort-of one. It's basically about returning traditional error values, but forcing the user to be explicit if she really wants to summarily discard them.
Whatever replaces exceptions better be darned good, otherwise I will be very unhappy. Explicit exception handling has its place, but in other cases it just uglifies code for no good reason. I don't really buy the performance argument: if your profiler pinpoints try/catch blocks as the bottleneck, it's not hard to refactor it to use error codes instead. Anything else is premature optimization. We should not uglify code just for some idealism that exceptions are somehow "bad". T -- EMACS = Extremely Massive And Cumbersome System
Sep 11
next sibling parent jfondren <julian.fondren gmail.com> writes:
On Sunday, 12 September 2021 at 01:50:37 UTC, H. S. Teoh wrote:
 On Sat, Sep 11, 2021 at 08:13:07PM +0000, Dukc via 
 Digitalmars-d wrote:
 On Friday, 10 September 2021 at 23:20:17 UTC, IGotD- wrote:
 
 Walter mentioned that exceptions are on the way out because 
 they are expensive and inhibits the optimizer which are fair 
 points. I interpreted this that Walter wants to move away 
 from exceptions in D.  Since then I haven't seen any 
 proposal for any new error handling for D.
Well the ` nodiscard` proposal, that is very likely to get accepted, is sort-of one. It's basically about returning traditional error values, but forcing the user to be explicit if she really wants to summarily discard them.
Whatever replaces exceptions better be darned good, otherwise I will be very unhappy. Explicit exception handling has its place, but in other cases it just uglifies code for no good reason.
Rust's monadic error handling is frequently ugly, but there's a cultural backing to it; the feature itself doesn't demand all that ugliness. As soon as you change the culture you can have solutions like ```d // halt program on empty nums int find_gcd(const int[] nums) nothrow { import std.numeric : gcd; import mod.algorithm : minElement, maxElement; return gcd(nums.minElement.unwrap, nums.maxElement.unwrap); } ``` vs. ```d // return None on empty nums with ? syntax auto find_gcd(const int[] nums) nothrow { import std.numeric : gcd; import mod.algorithm : minElement, maxElement; return gcd(nums.minElement?, nums.maxElement?); } ``` vs. ```d // throw catchable exception on empty nums int find_gcd(const int[] nums) { import std.numeric : gcd; import mod.algorithm : minElement, maxElement; return gcd(nums.minElement, nums.maxElement); } ``` with the compiler or some `alias this` magic inserting code that checks the Option!int returns and throwing on None. i.e., if you want it you get the explicit error handling, and if you don't want it you get exceptions, and you can guard against unwanted 'helpful' automatic exceptions with `nothrow`.
Sep 11
prev sibling next sibling parent Paulo Pinto <pjmlp progtools.org> writes:
On Sunday, 12 September 2021 at 01:50:37 UTC, H. S. Teoh wrote:
 On Sat, Sep 11, 2021 at 08:13:07PM +0000, Dukc via 
 Digitalmars-d wrote:
 On Friday, 10 September 2021 at 23:20:17 UTC, IGotD- wrote:
 
 Walter mentioned that exceptions are on the way out because 
 they are expensive and inhibits the optimizer which are fair 
 points. I interpreted this that Walter wants to move away 
 from exceptions in D.  Since then I haven't seen any 
 proposal for any new error handling for D.
Well the ` nodiscard` proposal, that is very likely to get accepted, is sort-of one. It's basically about returning traditional error values, but forcing the user to be explicit if she really wants to summarily discard them.
Whatever replaces exceptions better be darned good, otherwise I will be very unhappy. Explicit exception handling has its place, but in other cases it just uglifies code for no good reason. I don't really buy the performance argument: if your profiler pinpoints try/catch blocks as the bottleneck, it's not hard to refactor it to use error codes instead. Anything else is premature optimization. We should not uglify code just for some idealism that exceptions are somehow "bad". T
Yes, one thing that gets lost is the boilerplate required to create error types, to the point that there are crates to work around it. https://users.rust-lang.org/t/how-to-reduce-boilerplate-when-wrapping-errors/39363/6 Beware what you wish for, the grass is not always greener on the other side.
Sep 11
prev sibling parent reply IGotD- <nise nise.com> writes:
On Sunday, 12 September 2021 at 01:50:37 UTC, H. S. Teoh wrote:
 Whatever replaces exceptions better be darned good, otherwise I 
 will be very unhappy.  Explicit exception handling has its 
 place, but in other cases it just uglifies code for no good 
 reason.  I don't really buy the performance argument: if your 
 profiler pinpoints try/catch blocks as the bottleneck, it's not 
 hard to refactor it to use error codes instead. Anything else 
 is premature optimization.  We should not uglify code just for 
 some idealism that exceptions are somehow "bad".


 T
What about the C++ approach, return values that look like exceptions. You probably have already seen it. http://open-std.org/JTC1/SC22/WG21/docs/papers/2019/p0709r4.pdf We are allowed to "steal" from C++ if it is any good. Also it can be good for interoperability.
Sep 12
next sibling parent rikki cattermole <rikki cattermole.co.nz> writes:
On 13/09/2021 3:53 AM, IGotD- wrote:
 On Sunday, 12 September 2021 at 01:50:37 UTC, H. S. Teoh wrote:
 Whatever replaces exceptions better be darned good, otherwise I will 
 be very unhappy.  Explicit exception handling has its place, but in 
 other cases it just uglifies code for no good reason.  I don't really 
 buy the performance argument: if your profiler pinpoints try/catch 
 blocks as the bottleneck, it's not hard to refactor it to use error 
 codes instead. Anything else is premature optimization.  We should not 
 uglify code just for some idealism that exceptions are somehow "bad".


 T
What about the C++ approach, return values that look like exceptions. You probably have already seen it. http://open-std.org/JTC1/SC22/WG21/docs/papers/2019/p0709r4.pdf We are allowed to "steal" from C++ if it is any good. Also it can be good for interoperability.
I recently posted a similar design here: https://forum.dlang.org/post/she82k$g31$1 digitalmars.com A big difference between the one you linked and mine is the throws keyword takes the types that can be thrown. So an empty set of throwables, is the same as none being thrown. The other difference is to recognize that it is not a return value, rather it is an out parameter instead that is hidden like this is. This allows for the purpose of playing nicely with existing code.
Sep 12
prev sibling next sibling parent reply Alexandru Ermicioi <alexandru.ermicioi gmail.com> writes:
On Sunday, 12 September 2021 at 15:53:16 UTC, IGotD- wrote:
 What about the C++ approach, return values that look like 
 exceptions.

 You probably have already seen it.
 http://open-std.org/JTC1/SC22/WG21/docs/papers/2019/p0709r4.pdf

 We are allowed to "steal" from C++ if it is any good. Also it 
 can be good for interoperability.
Feels like java throws statement, except the need to handle exception in the function that calls throwing function.
Sep 12
parent reply Paulo Pinto <pjmlp progtools.org> writes:
On Sunday, 12 September 2021 at 22:50:49 UTC, Alexandru Ermicioi 
wrote:
 On Sunday, 12 September 2021 at 15:53:16 UTC, IGotD- wrote:
 What about the C++ approach, return values that look like 
 exceptions.

 You probably have already seen it.
 http://open-std.org/JTC1/SC22/WG21/docs/papers/2019/p0709r4.pdf

 We are allowed to "steal" from C++ if it is any good. Also it 
 can be good for interoperability.
Feels like java throws statement, except the need to handle exception in the function that calls throwing function.
Java throws statement was based on CLU, Modula-3 and C++, actually. Herb's idea is based on how Swift does exceptions, and ironically brings exception specifications back into C++, after their removal on C++17, just written in a different form.
Sep 12
parent Alexandru Ermicioi <alexandru.ermicioi gmail.com> writes:
On Monday, 13 September 2021 at 05:56:18 UTC, Paulo Pinto wrote:
 Java throws statement was based on CLU, Modula-3 and C++, 
 actually.

 Herb's idea is based on how Swift does exceptions, and 
 ironically brings exception specifications back into C++, after 
 their removal on C++17, just written in a different form.
Sorry, for confusion, I didn't refer to throws statement in body of a method, but rather throws statement in method declaration, used to list checked exceptions thrown by the method. I think they also can be called an exception specification. They enforce the caller of method to handle those cases, while in C++ spec, the purpose is performance, and unification of error reporting format.
Sep 15
prev sibling parent reply Paulo Pinto <pjmlp progtools.org> writes:
On Sunday, 12 September 2021 at 15:53:16 UTC, IGotD- wrote:
 On Sunday, 12 September 2021 at 01:50:37 UTC, H. S. Teoh wrote:
 Whatever replaces exceptions better be darned good, otherwise 
 I will be very unhappy.  Explicit exception handling has its 
 place, but in other cases it just uglifies code for no good 
 reason.  I don't really buy the performance argument: if your 
 profiler pinpoints try/catch blocks as the bottleneck, it's 
 not hard to refactor it to use error codes instead. Anything 
 else is premature optimization.  We should not uglify code 
 just for some idealism that exceptions are somehow "bad".


 T
What about the C++ approach, return values that look like exceptions. You probably have already seen it. http://open-std.org/JTC1/SC22/WG21/docs/papers/2019/p0709r4.pdf We are allowed to "steal" from C++ if it is any good. Also it can be good for interoperability.
That will probably never happen. First, a prototype has been requested to prove the design, before going forward, so C++26 might be the next time they will actually look at it. Secondly, Bjarne has written a rebutall that the biggest issue with exceptions is that compiler vendors just don't care to optimize the implementation, because of the usual ABI discussions, http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1947r0.pdf So I really don't expect this to ever be voted in into ISO C++, just like the reflection prototype and metaclasses proposals before it.
Sep 12
parent IGotD- <nise nise.com> writes:
On Monday, 13 September 2021 at 06:05:47 UTC, Paulo Pinto wrote:
 That will probably never happen.

 First, a prototype has been requested to prove the design, 
 before going forward, so C++26 might be the next time they will 
 actually look at it.

 Secondly, Bjarne has written a rebutall that the biggest issue 
 with exceptions is that compiler vendors just don't care to 
 optimize the implementation, because of the usual ABI 
 discussions,

 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1947r0.pdf

 So I really don't expect this to ever be voted in into ISO C++, 
 just like the reflection prototype and metaclasses proposals 
 before it.
That still doesn't prevent D from implementing the design. D can actually introduce it before C++. Implementation wise it could be complicated for D. Usage wise it fits D quite well as you can still use the exception syntax with the convenient scope guards.
Sep 13