February 25, 2022
On Thursday, 24 February 2022 at 23:59:56 UTC, meta wrote:
>
> We no longer live in an era with single cores!

yeah. but that's been the case for well over 20 years now ;-)

so I don't see that as an argument, in itself.
February 24, 2022
On Thu, Feb 24, 2022 at 11:59:56PM +0000, meta via Digitalmars-d wrote: [...]
> Nobody said to verbatim copy what C does, I personally explicitly said to take that idea and evolve it, just like the newer modern languages are properly doing (Go, Zig, Rust, Odin)

Go, Zig, Rust, and Odin all require explicit checking for error returns. This makes them suffer from exactly the same problem as my C example: after every single function call you must explicitly check for error conditions.  It doesn't matter what the implementation mechanism is, whether it's an int return, or a sum type, or a discriminated union, or a second return value. The point is that you have to check this error code *explicitly*, on *every single function call*.

This kind of error handling approach is fundamentally flawed because it incentivizes programmers to do the absolute minimum they can to shove the error under the carpet so that they can get on with making progress in the problem domain. Like ignore the error return value if they can, write generic "if error ignore it" boilerplate around their code.  Or just suffer from .unwrap hell.  It clutters code with error-handling paraphrenalia, making it needlessly verbose, and less composable.

For example, if you have function f that calls g and h, and g and h have their own set of possible error enums, then what should f return?  You either create yet another enum that subsumes both (and the caller then has the hairy task of making sense of which error codes comes from where and what to do with it), or, if you have to do this for all 65,535 functions in your entire codebase, just give up and go to a global binary {ok, fail} enum. Or, to harken back to my C example, OK and INTERNAL_ERR, which is where all return-code-based error handling inevitably gravitates towards. Which is about as useful as a BSOD every time the program encounters the slightest difficulty.

A thrown exception allows most code to eliminate all error-handling paraphrenalia, making code more readable and maintainable, and at the same time any actual errors are not ignored by default, you have to handle them somewhere up the call stack. And the exception contains specific information about the problem, not just the equivalent of INTERNAL_ERR (or worse, that stinky antipattern of storing an error message in a global variable that you then have to call some API function to extract).


> Tagged Unions, Multiple return type, enum/type inference, constructs to bubble up things etc.. we must move forward!

I've yet to see a convincing example of error handling based on tagged unions / multiple returns / etc., that doesn't suffer from the explicit handling problem I describe above.  If you have one, please enlighten me.


> I suspect in the near future, exception handling will be seen as very bad practice, if not already, we must prepare..

I've always been a skeptic of reacting to hypothetical futures that have not be proven is inevitable.


> We no longer live in an era with single cores!

Nobody has yet answered my initial question about whether D's exception throwing mechanism holds a global lock, which is the primary issue identified in the OP's linked article.  Until this question is answered, all this talk about exceptions being bad for multi cores is just beating a strawman.  It's tantamount to saying, since the global lock in C++ exceptions causes performance problems, exceptions must therefore be bad (even when no global lock exists).  It's a non sequitur.  The problem observed in the article is caused by the global lock, not by exceptions per se.  (And whether exceptions in and of themselves necessitate a global lock has not yet been established.)


T

-- 
May you live all the days of your life. -- Jonathan Swift
February 25, 2022
On Friday, 25 February 2022 at 00:30:49 UTC, H. S. Teoh wrote:
> Nobody has yet answered my initial question about whether D's exception throwing mechanism holds a global lock, which is the primary issue identified in the OP's linked article.  Until this question is answered, all this talk about exceptions being bad for multi cores is just beating a strawman.  It's tantamount to saying, since the global lock in C++ exceptions causes performance problems, exceptions must therefore be bad (even when no global lock exists).  It's a non sequitur.  The problem observed in the article is caused by the global lock, not by exceptions per se.  (And whether exceptions in and of themselves necessitate a global lock has not yet been established.)

I don't even think the global lock is the only issue, to begin with..

And, no matter what, it's a double penalty in D because it is a Heap allocation, and that can pause all the threads if it requires more memory.. due to the use of a GC

To counter that, one suggested the use of RC, or a value.. but even then, we still deal with exceptions!

No matter what, I personally stopped using exception altogether in my C++ projects, and moving forward, in my new D projects too


Also I don't think it is wise to design, or hold designs of, the language because you expect people to be lazy, we should have design philosophy based on HW and Software constrains, not based on lazyness/sloppyness of some developers.. we'll attract the wrong user base.. and even then it'll be hard to compete with sloppy dynamic languages out there... javascript... to name a few

We should all handle our errors and maintain healthy codebases that are safe and make software great again!

February 25, 2022
>

I've yet to see a convincing example of error handling based on tagged unions / multiple returns / etc., that doesn't suffer from the explicit handling problem I describe above. If you have one, please enlighten me.

void parseData() {

    string content = null;

    try {
        content = readFile("poem.txt");
    }
    catch (IOException e) {
        return false;
    }

    if (content.length == 0)
        throw new Exception("No data");

    // parse
}



// either bubble up the exception or handle it
try
{
    parseData();
}
catch ()
{}

vs

fn parseData() !void {

    var content = try readFile("poem.txt") catch {
        // handle error
        IOError => {},
    };

    if (content.len == 0) return err.NoData


   // parse
}


// either bubble up the error or handle it
try parseData();

(I'm not sure if that is the right syntax, It's been a while I haven't looked at zig)

While the zig version is not perfect, it is nicer, no extra indentation for try/catch, you can bubble up the error just like with an Exception, you avoid the cost and inconvenience of using Exceptions!

It's explicit, and you get to choose if you want to handle them.. or not! but at some points you will have to! that's what I call a healthy way to manage a code base!

They use enum for errors, and you can extend them if I remember correctly

Having multiple return value would make it even better, I'm a fan of Go's way, despite what people said about it!

Plus, they are looking to improve it https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback

February 25, 2022

Some interesting points: https://www.lighterra.com/papers/exceptionsharmful/

February 24, 2022
On 2/24/2022 3:44 PM, H. S. Teoh wrote:
> On Thu, Feb 24, 2022 at 02:42:14PM -0800, Walter Bright via Digitalmars-d wrote:
>> On 2/24/2022 12:01 PM, H. S. Teoh wrote:
>>> The problem with replacing exceptions with return codes is that it
>>> requires writing tons of boilerplate to handle error codes.
>>
>> I specifically said not using return codes, and gave examples.
> 
> It amounts to the same thing: the caller must explicitly handle all
> error conditions.

Not if you do it right. It's a challenge, I know.

The NaN result from floating point errors is an example. You can continue using the value without checking - the error nicely propagates with no effort or cost.


> Also, redesigning the problem like using the replacement character on
> invalid UTF-8 does not always make sense.  Sometimes you *want* your
> code to abort upon failure rather than silently substitute a nonce value
> in your data. E.g., if you're deep inside some parsing function and
> encounter a UTF-8 encoding problem, you do not want it to just insert
> some replacement data that isn't part of the input stream; you want it
> to bail out and abort the entire operation with a proper error message.

Interestingly, it seems one almost never wants to do this. For those that do, running a validator over your string first is a reasonable solution.

We do another technique inside D. The error is printed when the error is found, then a special AST error node is created, which is just ignored by everything else. It works out reasonably well.

February 25, 2022
On Friday, 25 February 2022 at 00:30:49 UTC, H. S. Teoh wrote:
> For example, if you have function f that calls g and h, and g and h have their own set of possible error enums, then what should f return?  You either create yet another enum that subsumes both (and the caller then has the hairy task of making sense of which error codes comes from where and what to do with it), or, if you have to do this for all 65,535 functions in your entire codebase, just give up and go to a global binary {ok, fail} enum. Or, to harken back to my C example, OK and INTERNAL_ERR, which is where all return-code-based error handling inevitably gravitates towards. Which is about as useful as a BSOD every time the program encounters the slightest difficulty.

You can also make your errors dynamically typed. Returning exceptions or strings would be ways to achieve this. That way, when handling errors, you handle errors you know. If it's some unknown exception or error code in a string, you simply return it and let the caller handle it. Same as with throwing exceptions, except it's manual.

You're right though that being manual is sometimes a downside. On the upside, you know when you're using a function that might error, because you have to write that error handling wrapper, so you don't just accidently propagate that error. But there's a temptation to just assert false on an error that could be handled or worse, pretend it didn't happen. Especially if you would have to change type of the function return to account for errors.

Same thing when reading error-handling code. On the other hand it's clutter that obfuscates what the code normally does, but at least you're not oblivious to the fact that there's potential for abnormal situations.

I suspect that an already written codebase is better off with returned error values, assuming they are implemented so that they cannot be implicitly discarded. But exceptions are still a good choice, because it's less work to implement error handling in the first place with them, than with error values.


February 25, 2022

On Thursday, 24 February 2022 at 20:27:50 UTC, forkit wrote:

>

Replacing exceptions with some radical change, will need to be well justified, and nobody (as far as I can tell), has done that.

Yes, the whole discussion is bogus. The only reason to not use exceptions in C++ is in embedded or in inner loops (where you typically don't need them anyway). The motivation for providing an alternative to exceptions in C++ is that not having an embedded-friendly standard library is problematic. It is harmful to their eco system.

D needs to focus on core issues. This is not a core issue for D. D needs to focus on usability and memory management issues first, which is hurting D's eco system.

Or, just focus in general… Being all over the map all the time is not a good thing for a design process.

February 25, 2022
On Friday, 25 February 2022 at 00:30:49 UTC, H. S. Teoh wrote:
>
> Go, Zig, Rust, and Odin all require explicit checking for error returns. This makes them suffer from exactly the same problem as my C example
>


This is not entirely true, at least for Rust.

Ex.

```
fn foo(i: i32) -> Result<i32, Error> {
    if i % 2 == 0 {
        Ok(i)
    } else {
        Err(/* ... */)
    }
}

fn bar(i: i32) -> Result<i32, Error> {
    let i = match foo(i) {
        Ok(i) => i,
        Err(e) => return Err(e),
    };

    // do something with i
}
```

The function bar can be reduced to:

```
fn bar(i: i32) -> Result<i32, Error> {
    let i = foo(i)?; // The magic happens here

    // do something with i
}
```
February 25, 2022
On Thursday, 24 February 2022 at 20:01:20 UTC, H. S. Teoh wrote:
>
> The problem with replacing exceptions with return codes is that it requires writing tons of boilerplate to handle error codes. I work with C at work every day, and eventually every function starts looking like this:
>
> 	int some_func(some_type_t *some_args) {
> 		int ret = SOME_FUNC_ERR;
>
> 		if ((ret = do_something(...)) != DO_SOMETHING_OK)
> 			goto EXIT;
>
> 		if ((ret = do_something_else(...)) != DO_SOMETHING_ELSE_OK)
> 			goto EXIT;
>
> 		if ((ret = do_yet_another(...)) != DO_YET_ANOTHER_OK)
> 			goto EXIT;
>
> 		// ... ad nauseaum
>
> 		ret = SOME_FUNC_OK;
> 	EXIT:
> 		return ret;
> 	}
>
> Every single function call must be wrapped with `if ((ret = blah(bleh)) == bluh)` boilerplate,...etc..

Not a comment about your point in general but, yeah, even though I agree with your point, in this specific example, you could have avoided all the gotos:

int some_func(some_type_t *some_args) {
	int ret = SOME_FUNC_ERR;

	if ((ret = do_something(...)) != DO_SOMETHING_OK)
		return SOME_FUNC_ERR;

	if ((ret = do_something_else(...)) != DO_SOMETHING_ELSE_OK)
		return SOME_FUNC_ERR;

	if ((ret = do_yet_another(...)) != DO_YET_ANOTHER_OK)
		return SOME_FUNC_ERR;


	// ... ad nauseaum

	return SOME_FUNC_OK;
}

But, where I have found it unavoidable (in C) to use this 'goto' style (and which - I'm certain - is where your example originates from) is when you have some common cleanup to do at the end:

int some_func(some_type_t *some_args) {
	int ret = SOME_FUNC_ERR;

	if ((ret = do_something(...)) != DO_SOMETHING_OK)
		goto EXIT;

	if ((ret = do_something_else(...)) != DO_SOMETHING_ELSE_OK)
		goto EXIT;

	if ((ret = do_yet_another(...)) != DO_YET_ANOTHER_OK)
		goto EXIT;

	// ... ad nauseaum

	ret = SOME_FUNC_OK;
EXIT:
	// ...some cleanup here

	return ret;
}

I have yet to find a way to avoid this in C. OTOH in C++/D/etc exceptions are just such a super convenient way to handle this case. In code where you are not concerned with the cost of exceptions or optimization I'll really miss them.