8 ms·
Say you have some code like, auto button = Button::create(); window.addSubview(button); button.setLabel("test"); button.setAlignment(Alignment:
by panic 8y ago
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.
- ben0x539 8y 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 8y 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 8y 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 8y 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 8y 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 8y 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 8y 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 8y 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 8y 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 8y 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 8y agoSolution: Button::create() returns a unique_ptr which you have to move to the window at the end.
- Too 8y agoThat is not RAII, that example is Acquiring the Resource before Initializing it.
- golergka 8y 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 8y agoRust does not have constructors as a language feature like that, and you must fully initialize objects, unless you use unsafe.
- golergka 8y 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 8y 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.