7 ms·
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 t
by Fronzie 9y ago
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 ?
- panic 9y agoSay 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.
- ben0x539 9y agoWouldn'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)?
- zaphar 9y agoAny 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.
- camgunz 9y agoSometimes 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.
- Fronzie 9y agoIf 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.
- panic 9y agoThe 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.
- alkonaut 9y agoI 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.
- twic 9y agoLooking 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=cb1db59a3ee1f4bad0a5472267ce8ec5&version=stable https://play.rust-lang.org/?gist=cb1db59a3ee1f4bad0a5472267c... ) In that situation, the button's destructor absolutely could delete itself.
- heavenlyhash 9y ago> 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?
- ben0x539 9y agoimo 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.
- emn13 9y agoSo, 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).
- jhasse 9y agoSolution: Button::create() returns a unique_ptr which you have to move to the window at the end.
- Too 9y agoThat is not RAII, that example is Acquiring the Resource before Initializing it.
- golergka 9y agoThe 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.
- steveklabnik 9y agoRust does not have constructors as a language feature like that, and you must fully initialize objects, unless you use unsafe.
- golergka 9y agoWouldn'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.
- fooker 9y ago>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.
- jstimpfle 9y ago> 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.
- falcolas 9y agoAt least with exceptions, “forgetting to handle the error” results in a predictable bubbling of the exception, as opposed to continuing with an unknown state.
- bbatha 9y agoThat’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.
- falcolas 9y agoThe parent was referring to C style error handling, not Rust.
- 9y ago
- prevedmedved 9y agoThe 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.
- heavenlyhash 9y agoI 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"!
- SAI_Peregrinus 9y agoExceptions 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.
- twic 9y agoThey 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.
- zaphar 9y agoThat 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.