News for hackers. Stuff that matters.
Re:Dark side of ergonomics in Rust by Fronzie on Saturday April 14, 2018 @04:48AM (#16836535)
The dislike of exceptions seems more common.

It puzzles me a bit. For numerical software, exceptions work quite well: for performance, all allocations are in the constructors, so the RAII idiom fits naturally. Using exceptions saves from writing a great deal of error-handling, which easily can have errors itself.

Has anyone seen properly-RAII-ed code where exceptions still have drawbacks ?

Re:Dark side of ergonomics in Rust by panic on Saturday April 14, 2018 @05:50AM (#16836655)
Say you have some code like,

    auto button = Button::create();
    window.addSubview(button);
    button.setLabel("test");
    button.setAlignment(Alignment::TOP_LEFT);
    button.sizeToFit();
If setLabel() throws an exception, you'll end up with a half-initialized button in the view hierarchy.

For exceptions to work predictably, all mutation within a try-catch block should be captured in a transaction that can be rolled back. Then you don't need to worry about each function possibly throwing an exception. Either the entire block commits its changes, or it's as if the entire block didn't happen at all.

Re:Dark side of ergonomics in Rust by ben0x539 on Saturday April 14, 2018 @09:54AM (#16837317)
Wouldn't this be fixed simply by calling `addSubview` last (and designing your UI toolkit in such a way that you don't need to imperatively call `sizeToFit` after adding a thing to a container, but implicitly sizing things correctly during layout)?
Re:Dark side of ergonomics in Rust by camgunz on Saturday April 14, 2018 @11:53AM (#16837834)
Sometimes whatever `window` and `button` actually are have references to each other (ex: parent/children relationships) so it's possible -- in fact it's surprisingly common that -- there is no correct order.

You need total knowledge of what can throw exceptions, and while the same is true for status codes, the raison d'etre for exceptions is shrinking the amount of error handling in your code. If you're checking for exceptions at the same rate you'd be checking for status codes, exceptions are pointless.

But generally, your suggestion to change the architecture points to what the problem with these "ergonomic" changes engender. You more or less never have to rearchitect or refactor things when using status codes, and rearchitecting and refactoring are error prone and generally hugely fraught. The cognitive load of things like exceptions leads to lower productivity or lower quality, because you only have so many brain cycles and something has to give.

I think ergonomic changes are helpful to get more dynamic programmers into stricter languages like Rust. But I think we should keep in mind that typing and boilerplate are really never our problems and prioritize accordingly.

Re:Dark side of ergonomics in Rust by zaphar on Saturday April 14, 2018 @11:08AM (#16837620)
Any solution that boils down to just get everyone who writes code you have to interact with to always write the code correctly is doomed to failure.

Rust tries to make it impossible in the general cases to do the wrong thing. And gives you an unsafe escape hatch for when you need to do something the compiler won't allow. The result is vastly safer code because the compiler guards against the most typical human failings.

Re:Dark side of ergonomics in Rust by heavenlyhash on Saturday April 14, 2018 @06:17AM (#16836724)
> For exceptions to work predictably, all mutation within a try-catch block should be captured in a transaction that can be rolled back.

Wouldn't that just be beautiful?

Re:Dark side of ergonomics in Rust by the_grue on Saturday April 14, 2018 @08:14AM (#16837005)
That sounds like a natural fit for Rust's lifetime-tracking mechanism. The compiler can automatically drop all initialized objects upon exception. In fact, I'm reasonably sure it does just that for panic handling.
Re:Dark side of ergonomics in Rust by panic on Saturday April 14, 2018 @08:34AM (#16837043)
That example is initialization code, but you'd run into the same problem even if the control were already initialized (and you were trying to set new properties on it). In that case, you'd likely have a borrowed pointer, so it wouldn't be dropped when the panic happened.
Re:Dark side of ergonomics in Rust by ordu on Saturday April 14, 2018 @09:40AM (#16837274)
The pointer can be dropped. The pointed object cannot. So the problem of rolling back the transaction changes ownership, now our button should do sensible things in setLabel, to rollback changes made before an exception.
Re:Dark side of ergonomics in Rust by ben0x539 on Saturday April 14, 2018 @10:02AM (#16837344)
imo the dream of exceptions is deferring error handling so happy-path code can be straightforward and free of worries, but if every line of code indirectly under a try-catch block has to embody transactional thinking to enable proper RAII behavior, that seems like a huge obligation.
Re:Dark side of ergonomics in Rust by emn13 on Saturday April 14, 2018 @01:10PM (#16838244)
So, that's the bad-stuff perspective. But transactions in actual DB's show it doesn't have to be that way, and transaction isolation is likely to be easier in a language than a DB because you typically communicate via a fairly narrow, well-defined channel, and not via a morass of side-effects.

Another good-stuff perspective are processes or equivalents (e.g. erlang's internal processes or docker containers). Happy-path code can ignore errors; sufficiently serious errors tear down the "process" and the calling code can decide how to deal with that (ideally without cleanup responsibilities).

As long as indirect effects are well-contained, there's nothing necessarily wrong with ignoring errors. Especially when fine-grained error handling simply isn't interesting, it can be a relief simply to ignore errors. E.g. DB style fine-grained locking is really only possible because you don't need to manually deal with each and every possible failure moment (because there are unbelievably many).

Re:Dark side of ergonomics in Rust by ben0x539 on Sunday April 15, 2018 @08:37AM (#16842406)
yeah i agree that seems like a generally desirable state of affairs.

not sure if i'd sign the statement "you typically communicate via a fairly narrow, well-defined channel, and not via a morass of side-effects" though ;)

Re:Dark side of ergonomics in Rust by emn13 on Sunday April 15, 2018 @02:26PM (#16844073)
Hey, it's aspirational ;-).
Re:Dark side of ergonomics in Rust by anacrolix on Saturday April 14, 2018 @10:24AM (#16837425)
Haskell's STM monad.
Re:Dark side of ergonomics in Rust by fooker on Saturday April 14, 2018 @03:57PM (#16839141)
>For exceptions to work predictably, all mutation within a try-catch block should be captured in a transaction that can be rolled back.

[code] try { system("rm foo.txt");} catch (...) {} [/code]

Not everything can be rolled back. Often, automatic rollbacks lead to states unaccounted for by the developer.

Re:Dark side of ergonomics in Rust by Too on Saturday April 14, 2018 @01:51PM (#16838461)
That is not RAII, that example is Acquiring the Resource before Initializing it.
Re:Dark side of ergonomics in Rust by jhasse on Saturday April 14, 2018 @12:41PM (#16838095)
Solution: Button::create() returns a unique_ptr which you have to move to the window at the end.
Re:Dark side of ergonomics in Rust by golergka on Saturday April 14, 2018 @01:18PM (#16838272)
The problem is that you can have half-initialzed objects at all. I don't know Rust very well yet, but in a typical OOP language it's a constructor's job to completely initialize an object, and exception in constrctor causes object to not be created at all.
Re:Dark side of ergonomics in Rust by steveklabnik on Saturday April 14, 2018 @01:22PM (#16838291)
Rust does not have constructors as a language feature like that, and you must fully initialize objects, unless you use unsafe.
Re:Dark side of ergonomics in Rust by golergka on Saturday April 14, 2018 @02:52PM (#16838809)
Wouldn't you want, in a similar manner, a single create() call to be responsible for full initialization? An API that makes it possible to have entities half-initialzed looks like a design mistake to me. If an initialization is long and requires a lot of parameters, I personally split a "config" object that has easy access to all of them and the "real" object that swallows it in constructor or "create" call all at once.
Re:Dark side of ergonomics in Rust by Fronzie on Saturday April 14, 2018 @06:15AM (#16836718)
If setLabel() throws an exception, the scope is left, cleaning up button, which then no longer exists. The only try/catch blocks are where you can do anything usefull. That's typically only at the highest level of the application.

Exceptions depends on RAII, that's true.

Re:Dark side of ergonomics in Rust by panic on Saturday April 14, 2018 @06:19AM (#16836728)
The button has to exist after leaving the scope, since the whole point is to create it and add it to the window. It can't remove itself when the function returns.

You could make this general idea work, though, by adding a "button.commit();" at the end of the function. The button destructor would remove itself unless commit() had been called -- basically an ad-hoc transaction on the level of a single button.

Re:Dark side of ergonomics in Rust by twic on Saturday April 14, 2018 @07:45AM (#16836928)
Looking at this from a Rusty point of view, ownership rules make this possible. When this function creates but button, it owns it. To mutate it (addLabel/setAlignment/sizeToFit), it has to own it. But for it to continue to exist after the function returns, something else has to own it. Therefore, Window::addSubview has to take ownership of the button, and so, has to come last:

  fn make_button(window: &mut Window) {
      let mut button = Button::create();
      button.set_label("test");
      button.set_alignment(Alignment::TopLeft);
      button.size_to_fit();
      window.add_subview(button);
  }
(complete toy example at https://play.rust-lang.org/?gist=cb1db59a3ee1f4bad0a5472267c... )

In that situation, the button's destructor absolutely could delete itself.

Re:Dark side of ergonomics in Rust by alkonaut on Saturday April 14, 2018 @06:45AM (#16836775)
I know it's a toy example, but an exception in such a method doesn't seem like something that can be handled/retried like an IO problem. Any such exception should probably be handled by simply tearing down the app anyway.
Re:Dark side of ergonomics in Rust by jstimpfle on Saturday April 14, 2018 @06:40AM (#16836766)
> Has anyone seen properly-RAII-ed code where exceptions still have drawbacks?

The drawback is "properly-RAII-ed code". It's hard to write because where you write the RAIIed classes you don't have the context of the place / places where it's used. It's extremely hard to read and step through with a debugger - because control flow is all over the place. I don't want to write classes instead of simple regular control flow.

And regarding standard control flow code, I've never seen a code base that handles exceptions correctly / elegantly / in a disciplined way. The only way to deal with exceptions is handling them immediately, i.e.

  try:
      f = open(filepath)
  except FileNotFoundError:
      handle_file_not_found()
To be honest I prefer C-style return code handling. It would be only 2 or 3 lines instead of these 4, and no additional indentation, and no additional value return mechanism. The only cost of C-style handling vs exceptions is duplicating a few library functions: Need to offer interfaces that die and ones that allow handling the error. And there's the possibility of forgetting to handle the error (if the caller considers it an error, after all) once in a long while. But you have that problem with exceptions, too...

In other words, exceptions are good for writing sloppy code (I like writing Python scripts, but I rarely handle exceptions), and very bad for serious code bases.

Re:Dark side of ergonomics in Rust by falcolas on Saturday April 14, 2018 @09:44AM (#16837283)
At least with exceptions, “forgetting to handle the error” results in a predictable bubbling of the exception, as opposed to continuing with an unknown state.
Re:Dark side of ergonomics in Rust by bbatha on Saturday April 14, 2018 @10:00AM (#16837333)
That’s already a property of rust errors. If a function errors you can’t use its return value until you handle the error some way either explicitly, forward it with the ? operator, or unwrap it indicating an unrecoverable error.
Re:Dark side of ergonomics in Rust by falcolas on Saturday April 14, 2018 @10:18AM (#16837396)
The parent was referring to C style error handling, not Rust.
Re:Dark side of ergonomics in Rust by xamuel on Saturday April 14, 2018 @10:52AM (#16837538)
People misuse exceptions. They use them as control-flow, like another form of "if...then", and as you point out, they're terrible for that.

The proper way to use exceptions is when you want behavior that bubbles up. So if you do x=f(g(h(x))) and h(x) fails because the user didn't configure something, you only need the config-checking logic in h, not in g or f. This use-case is quite rare though, and overall not worth the extra language complexity.

Re:Dark side of ergonomics in Rust by ZirconiumX on Saturday April 14, 2018 @11:19AM (#16837670)
If you don't mind me asking, what do you think of Go-style error handling where there are multiple return types: one exclusively for errors and however many for actual results. Is this better to your eyes?
Re:Dark side of ergonomics in Rust by jstimpfle on Saturday April 14, 2018 @11:54AM (#16837835)
I think there is not much difference. I used that approach myself once when writing a simple parser in python. I think it lacks some elegance, but it may be better than out-arguments (pointer argument to return value). On the other hand, the two-variables handling has the disadvantage that the "real" return variable is always (over)written, also in case of error.

I think there is one underused error handling mechanism: "asynchronous error handling" where you have to to ask for errors explicitly. Like in the OpenGL API, for example. It might be the best choice where there is a lot of communication between largely isolates states.

So that could be one answer: If possible design the API in such a way that functions that can have expected errors [1] do not return values that only make sense if there was no error. Because this means that control flow gets very irregular on the calling side. Such a design could be hard to do though, I don't know.

The other answer could be: Don't have errors that you don't know how to handle. Unless you write very low-level code, like a library (which means you probably want to delegate the handling), just die on errors that you don't know how to handle. Make the die() call a best-effort cleanup routine if possible, and keep the important to-be-cleaned up state as global as possible (NOT on the call stack), to support such a cleanup routine.

[1] I hate the vagueness of that term, but take it to mean "no statically obvious wrong usage by the caller", like passing in made-up or corrupted values

Re:Dark side of ergonomics in Rust by Too on Saturday April 14, 2018 @01:59PM (#16838514)
Cute example but what exactly does your handle_file_not_found() function do? Isn't it the caller further up the stack that knows best what to do? Assuming your example code is inside library_function_foo() and I'm a web server calling into library_function_foo() i know i should return http code 500 if "anything" throws an exception, if i reuse library_function_foo() in another command-line client project i might want it to print the error message to stdout instead.
Re:Dark side of ergonomics in Rust by jstimpfle on Saturday April 14, 2018 @02:19PM (#16838643)
> Isn't it the caller further up the stack that knows best what to do?

Often yes (one of the exceptions being a simple die() routine that cleans up only global state). It's just pseudocode, and in real code you will often not call any functions in the exception handling block.

Re:Dark side of ergonomics in Rust by anacrolix on Saturday April 14, 2018 @10:28AM (#16837440)
The flaw is in the language syntax. With a language that allows a lightweight, more general manipulation of flow you just get

    open filepath `catch` handle_file_not_found >>= use_file_handle
    handle_file_not_found :: FileNotFoundError -> IO a
    use_file_handle :: FileHandle -> IO a
Re:Dark side of ergonomics in Rust by jstimpfle on Saturday April 14, 2018 @10:41AM (#16837487)
Sure, I know Haskell too, and that code above is a mouthful... I'm undecided what is worse in terms of "control flow is all over the code base" and "code is hard to understand" - C++ RAII or Haskell error handling. Moreover the latter comes in a thousand incompatible flavours. Have fun writing Monad transformers :-). It's next to impossible to engineer a robust and readable/maintable application this way (besides simply dying on errors).
Re:Dark side of ergonomics in Rust by ensiferum on Saturday April 14, 2018 @08:16AM (#16837006)
Lol. All so wrong.
Re:Dark side of ergonomics in Rust by zaphar on Saturday April 14, 2018 @11:05AM (#16837602)
If you think it's all wrong then explain why. If all you want to do is signal in-group status or cargo-cult thinking then you'll get downvoted to oblivion. Hacker News has a pretty strongly developed immune response to this sort of comment.
Re:Dark side of ergonomics in Rust by prevedmedved on Saturday April 14, 2018 @06:51AM (#16836785)
The dislike comes from the fact that some people abuse exceptions as a flow control mechanism. This is due to the "everything is a nail if you have a hammer" problem: exceptions and RAII is a resource lifetime control mechanism that every modern and sane language should have, but very few programmers are ready to reason in terms of resources and lifetimes instead of instructions.
Re:Dark side of ergonomics in Rust by heavenlyhash on Saturday April 14, 2018 @07:24AM (#16836880)
I think part of the insidiousness of exceptions is that we're tempted to think there's a way to use them that's not as a flow control mechanism. And there isn't. That's what they do: _alter flow_.

A sibling comment hints at the way out:

> For exceptions to work predictably, all mutation within a try-catch block should be captured in a transaction that can be rolled back.

The instant an exception can be raised from an area in code is the instant you need to make that entire area into functionally-pure side-effect-free logic, so that you can reason about it even when the exception is raised.

And then you've got a function that could just as well return an Either<Result, Error> -- what a strange "coincidence"!

Re:Dark side of ergonomics in Rust by zbentley on Saturday April 14, 2018 @01:21PM (#16838287)
> The instant an exception can be raised from an area in code is the instant you need to make that entire area into functionally-pure side-effect-free logic

I mean, that would be lovely, but is really only feasible for exceptions raised from code that is operating exclusively on values in memory (NPEs, for example). Rather a lot of exceptions are raised from necessarily side-effectful operations, especially those dealing with I/O.

Since you can't predict the future, you can't guarantee purity/transactionality of code that raises that kind, so saying "code should be pure when it can throw" is a true-but-impossible statement. That's why most languages with an emphasis on purity tend to eschew exceptions and/or invert control flow such that effectful stuff happens externally to the functions handling its output (e.g. the Maybe or IO monads).

Re:Dark side of ergonomics in Rust by heavenlyhash on Sunday April 15, 2018 @08:26AM (#16842370)
This is correct, of course, but partial progress here is massive progress indeed.

The further you push the potentially complicated code towards functional purity, the more the I/O and other side-effecting/hard-to-roll-back things float to the top... and it becomes almost natural to group all of them together...

... into what you might almost be tempted to call a "transaction".

(Indeed, like the IO monad leads one to do. I've explicitly been trying to avoid saying the "m" word, though, because it seems to be a cognitive stop-sign for a lot of folks, for whatever reason.)

Re:Dark side of ergonomics in Rust by Too on Saturday April 14, 2018 @02:38PM (#16838758)
>>The instant an exception can be raised from an area in code is the instant you need to make that entire area into functionally-pure side-effect-free logic, so that you can reason about it even when the exception is raised.

Great insight. But i would say that explicit error handling doesn't help you much there. Side effects are hard and side effects can by nature normally not be reverted anyway. Deleted a file? Tough luck getting it back. IO and external interfaces could also be an issue, wrote 50% of a file to USB-stick when the user yanked it from the port? No amount of explicit error handling will help you remove it there.

Re:Dark side of ergonomics in Rust by SAI_Peregrinus on Saturday April 14, 2018 @09:49AM (#16837298)
Exceptions are effectively INTERCAL's "COME FROM".

If(error detected) { COME FROM (Error location); Handle Error }

It trades ease of writing error handling code for ease of debugging.

Re:Dark side of ergonomics in Rust by twic on Saturday April 14, 2018 @10:42AM (#16837492)
They aren't, though. COME FROM can come from anywhere; catch can only come from somewhere an exception actually got thrown. That is a night and day difference.
Re:Dark side of ergonomics in Rust by zaphar on Saturday April 14, 2018 @11:17AM (#16837661)
That is a difference without a practical consequence. The reality that unless you have only checked exceptions then effectively any line of code could throw an exception. And if that is so then you have to assume that the next line of code may not run. Just as Come From would cause the flow of control to mysteriously jump on you exception could as well. You are right that at least the stacks properly unwind with exceptions which disallow a certain class of bugs but the there is still a large body of bugs around the codes semantics itself that could occur.

Your best bet to maintain a clean state with exceptions is to never catch them and just let the whole process die. Otherwise there will be a never ending stream of bugs that your maintenance programmers will be dealing with. They'll be cursing exceptions the whole time.