Asked on Reddit: Which of Rust, D, Go, Nim, and Crystal is the strongest and why?

Idan Arye via Digitalmars-d digitalmars-d at puremagic.com
Thu Jun 11 17:50:56 PDT 2015


On Thursday, 11 June 2015 at 21:57:36 UTC, Dave wrote:
> In regards to being faster, I'm not a big fan of exceptions in
> the first place. This probably explains my perspective on them,
> but I am familiar with their typical use case. And it's to
> communicate errors. I'd much prefer something like what Andrei
> presented during one of his C++ talks (Expected<T>). If I were 
> to
> design a language today, I might try to incorporate that somehow
> with some semantic sugar and a "handle by default" mentality.

I've reordered your post a bit so I can refer to this part first. 
I think you and I refer to different things when we talk about 
returned errors, and with the definition I think you have in mind 
I do see how my arguments can look like the mumbling of a madman. 
So I'm going to clear it out first.

At a previous post 
here(http://forum.dlang.org/post/riiuqazmqfyftppmxxgz@forum.dlang.org), 
I've said that "I'm not talking about C style 
return-error-code-and-write-the-actual-result-into-a-variable-passed-by-reference 
- functional languages like Haskell, Scala and Rust have shown 
that monadic sum types of result/error are both safe(...) and 
easy to use(...)"

What I refer by "monadic sum types of result/error" is pretty 
similar in concept to Expected<T>, though what I in mind is much 
closer to what functional languages have, which makes allows both 
easier handling inside expressions and a guarantee that the 
underlying result will only be used in a path where it was 
checked that there is no error or after the user explicitly said 
it's OK to convert it to an exception.

This is what's done in Rust(except it's converted to panics, 
which are harder to log and contain than exceptions), it can be 
done in D, and of course a new language that chooses this 
approach can have syntax for it. Here is an example for how it 
would be used in D:

     Expected!int parseInt(string txt) {
         if (/*parsing successful*/) {
             return expected(parsedValue);
         } else {
             return error();
         }
     }

     // Propogation - functional style
     Expected!string formatNextValueText(string origNumber) {
         return parseInt(origNumber).ifOK!(
             // good path
             parsedValue => "After %s comes %s".format(
                 parsedValue, parsedValue + 1).expected,
             // error path
             () => error());
     }

     // Propogation - imperative style
     Expected!string formatNextValueText(string origNumber) {
         auto parsedValue = parseInt(origNumber);
         if (auto parsedValuePtr = parsedValue.getIfOK()) {
             return "After %s comes %s".format(
                 *parsedValuePtr, *parsedValuePtr + 1).expected,
         } else {
             return error();
         }
     }

     // Convertion to exception
     string formatNextValueText(string origNumber) {
         // This will throw an exception if the parsing, and
         // return the parsed value if it succeeded:
         auto parsedValue = parseInt(origNumber).enforceOK();
         return "After %s comes %s".format(
             *parsedValue, *parsedValue + 1);
     }


Now to answer the rest of your post, which actually came first:

>> He is saying that now anything that throws will not only be 
>> slow but also have the same limitations as returned errors.
>
> "nothrow by default is combining the slowness of exceptions with
> the limitness of returned errors."
>
> He literally said combine the slowness of exceptions. So I don't
> know how to read that other than he said it's slow. But perhaps 
> I
> am just misunderstanding his wording, so perhaps it's best I 
> just
> assume I misunderstood him.

I also literally said "with the limitness of returned errors.". 
That part is important part of the sentence. My point is that the 
exception mechanism is much less limited from the returned value 
mechanism, because it lets you handler the error in a higher 
level without modifying the middle levels to acknowledge it. The 
price for this is slowness.

The benefits of nothrow by default come naturally with returned 
errors - the users can't implicitly ignore them, and since the 
possible errors are encoded into the return type any tool that 
can display the return type can show you these errors.

With nothrow by default, you are paying the performance price of 
an exception mechanism, and then do extra work to add to it the 
limitations of returned errors, just so you can get the benefits 
that come naturally with returned errors. Wouldn't it be better 
to just use returned errors in the first place?

>> but also have the same limitations as returned errors
>
> That is a legitimate concern, but I don't think it is correct.
> The transitive nature would enforce that you at least handle it
> at some point along the chain. Nothing would force you to handle
> it right away. Although I think in most cases it's far better to
> do it when the error occurs(but this is my style). But when you
> don't there would be at least a flag saying "this might fail"
> that you and others could see. You can ignore that all the way 
> up
> the turtles, but at some point you are going to be like "I 
> should
> handle these errors".
>
>> If you have to acknowledge it anyways, then might as well just 
>> use returned errors because they are faster.
>
> I guess you could think of nothrow as an acknowledgement. I'd
> view it more like a warning to others. As I said before, I don't
> think you should be *forced* to do anything. Handle it right 
> away
> and you don't have to mark your own function as 'throw'. Don't
> handle it, then your function should be marked 'throw', and the
> compiler can help others out and tell them to expect to have to
> handle the exception at some point.

This is not very different than returned errors: you either 
handle the error or change the function's signature so it can 
pass the error to it's caller.

The big benefit of unchecked exceptions is that they require 
minimal effort to use - when I code and encounter a path that you 
can't handle, I just throw an exception(I can also use helper 
assert/enforce functions). I don't need to interrupt the flow and 
start adding `throws` all over the project, making my brain do a 
"context switch" that makes me forget the immediate thing I was 
working on, and increasing the chances of my commit conflicting 
with other concurrent commits(because it was touching general 
functions all over the place). No - I throw the exception, and 
leave worrying to how it will be handled to later.

The alternative is not that I leave my current task to implement 
something that'll handle the exception at the right place - the 
alternative is that I continue my current task without throwing 
the exception, and only deal with it later on when something 
crashes or doesn't work properly.

Why? Because I have a task to do, and if every time I encounter a 
place that *might* indicate an error I'll need to make sure that 
error is handled, I won't be able to focus on the actual task.

Out of the unhandled error possibilities, exceptions are easiest 
to fix when they do happen. Forbidding unchecked exceptions 
doesn't mean there are no errors - just that you don't have this 
amazing mechanism for tracking and debugging them.


More information about the Digitalmars-d mailing list