5 ms·
Rust Solves the Issues with Exceptions
- deleted 2y ago[deleted]
- xvector 2y agoI was following the article and nodding my head all the way up to the Rust section, at which point I lost track because I'm not too familiar with Rust :) It's also not clear to me how `if let , while let , let-else` help (and how you'd use them), or what an "ergonomic sum type" is and how it helps with exceptions. Would be really cool if there was a code snippet in Rust comparing/explaining the advantage over Java for each benefit.
- LAC-Tech 2y ago"if let" is rusts answer to Java's "isPresent" method. if let Some(val) = my_optional_value { // perform some side effect with val }
- brabel 2y agoIn the context of this blog post, an example with Result would be more suitable though. if let Ok(value) = fun_can_fail() { // success, use value } else { // error, but we don't care which one when using if-let }
- Expurple 2y agoThis isn't the only pattern that `if let` is suitable for. It's often used without an `else`, as a syntax sugar for match value { the_only_case_that_we_care_about => { // Do something with the bound value. }, _ => {}, // Don't do anything, we don't care about the other cases at all. }
- moomin 2y agoThe basic idea is that an exception is just data, so you can just treat it as part of the return type. Most of the rest of it is constructs that make it easy to work with, which mostly means making things behave similarly to the way exceptions work. But with the option to just treat it as regular data rather than complex catch try rethrow logic. Checked exceptions were inferior to this because they was outside of the type system; saying “This type or three possible exceptions” was so painful people stopped using. With this approach it’s just a type and has whatever name you like.
- Expurple 2y ago> “This type or three possible exceptions” was so painful people stopped using. A reader on Reddit has also pointed out [1] that it's literally impossible to represent "n possible exceptions" generically. Java makes you manually write out exactly 3 exceptions in the signature, or exactly 1 exception in the signature, or exactly 0 exceptions in the signature... [1] https://www.reddit.com/r/rust/comments/1h3kdye/rust_solves_the_issues_with_exceptions/lzsnn7g/ https://www.reddit.com/r/rust/comments/1h3kdye/rust_solves_t...
- Expurple 2y ago> Would be really cool if there was a code snippet in Rust comparing/explaining the advantage over Java for each benefit. Yeah, I thought about that. But the post kept and kept expanding in scope so I decided to instead hyperlink where I could. > It's also not clear to me how `if let , while let , let-else` help (and how you'd use them) All three of those are hyperlinks in the post. > what an "ergonomic sum type" is Here, I mean language support for defining and using sum types[1] without verbose code that doesn't reflect the programmer's intent well. A typical sum type implementation in Java uses a sealed interface. You can't implement interfaces on "foreign" types that you don't control, like `boolean`. You often have to define extra wrapper classes that distract you when navigating the codebase and also hurt performance: public sealed interface MutuallyExclusiveFlags permits FlagA, FlagB {} public record FlagA(boolean value) implements MutuallyExclusiveFlags {} public record FlagB(boolean value) implements MutuallyExclusiveFlags {} Meanwhile, in Rust: pub enum MutuallyExclusiveFlags { FlagA(bool), FlagB(bool), } No boilerplate, no extra types, no unnecessary performance cost. This whole object takes only 2 bytes of memory on the stack! [2] I didn't go into detail on this, because the post isn't about introducing sum types. > and how it helps with exceptions The part about "exceptional flow" describes some cases where Result-based error handling is preferable to using exceptions. Language support for sum types is necessary to properly express Result values and the sets of possible error values inside. [1] https://en.wikipedia.org/wiki/Tagged_union https://en.wikipedia.org/wiki/Tagged_union [2] https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=316ac4f5922988cffffeceb5c60fe59a https://play.rust-lang.org/?version=stable&mode=debug&editio...
- LAC-Tech 2y agoI actually miss exceptions sometimes in rust. I know all about Result, it's great for errors that are recoverable. But there's a lot of errors that aren't really recoverable inside a library, but you may not want to cause the code using that library to crash - perhaps your buggy component can be worked around. Ocaml has both algebraic data types and exceptions, and works really well.
- tmtvl 2y agoI believe Rust has ADTs as well, though I don't know whether it has Lisp-style ad-hoc ADTs, with which you could easily do something like: (defun* foo ((bar Bar)) (:returns (or Baz QuuxException ZotException YendorException)) ...) But in Rust syntax.
- brabel 2y agoRust doesn't support that, but there's an RFC trying to figure out how that could be done (hasn't gone anywhere after more than 10 years of discussions): https://github.com/rust-lang/rfcs/issues/294 https://github.com/rust-lang/rfcs/issues/294 But Rust supports macros, just like Lisp, so of course someone wrote a library that provides something similar: https://docs.rs/some-error/latest/some_error/ https://docs.rs/some-error/latest/some_error/ Their post about how they came up with this crate is quite interesting: https://jam1.re/blog/anonymous-sum-types-for-rust-errors https://jam1.re/blog/anonymous-sum-types-for-rust-errors
- tmtvl 2y agoCool, that's the value of languages with macros: adding useful features which help combat needless infrastructure code (AKA boilerplate).
- Expurple 2y agoYeah, but there's always a cultural struggle to prevent everyone from using their own incomprehensible DSL. See "The Lisp Curse" [1]. Even vanilla Haskell (without macros, i.e. without Template Haskell) takes it too far by allowing to define arbitrary custom operators with arbirtary associativity and precedence. Many Haskell libraries are an incomprehensible soup of symbols instead of regular functions with readable names. Rust does pretty well so far. Macros are used appropriately in the ecosystem. [1] http://www.winestockwebdesign.com/Essays/Lisp_Curse.html http://www.winestockwebdesign.com/Essays/Lisp_Curse.html
- unsnap_biceps 2y ago> Can you guess why I used an intermediate variable instead of calling f(g(x)) and f(gException) in try-catch? I can't guess why. Could someone explain why the intermediate variable is important?
- trav4225 2y agoClick the triangle icon next to that question to see the answer. :-)
- unsnap_biceps 2y agoAhh, thank you. I looked first on mobile and missed there was comments to the right that explained why. My mistake!
- brabel 2y agoWill people keep writing these annoying posts with no insight just to complain about Exceptions (and Java in general)? I've done a lot of Java. And quite a bit of Rust too. The least thing (or close to it) that annoys me with Java is Java's error handling. It only really makes me upset when a lambda needs to call some method that has a checked Exception in its signature, usually because it does IO... yeah that's annoying... but I still get the job done without losing more than 30 seconds. Otherwise, I just make a decision on whether to handle errors locally or propagate them, and that's all there's to it. Rust is nice and all. But the kind of annoyance I get with Result values are probably worse than is the case in Java with Exceptions. For example, as mentioned in the article, when you end up having multiple incompatible errors and you have to manually hack something, usually losing context, to make the compiler happy. Almost every project just adds yet another dependency on one of the error handling libraries (because this problem and similar ones, like losing the stack trace entirely, are all too common so there's lots of libraries to fix that) and you probably will end up with all of them in your project once you've written anything non-trivial. The problem with API changes in Java are real, but the same exact issue exists in Rust: you inevitably have to end up adding new error scenarios as you add features to a library. Instead of a new Exception, you just end up with a new Error variant in Result. Anyway, even though in the beginning I also got all euphoric with Rust, thinking it fixed every issue with every language, today I think it just shifted the sorts of problems you have to deal with. It definitely went in the right direction (specially with handling of closable resources and concurrency) and it's a better language in almost every way than Java and most languages. But sorry, with regards to error handling, it's a minor improvement at best.
- mrkeen 2y ago> it's a better language in almost every way than Java > with regards to error handling, it's a minor improvement Then let's keep pouring out these articles until we stop using a language that's worse in almost every way than Rust (and error handling that's a little bit worse).
- brabel 2y agoIt shouldn't be an objective to stop using any language. There's lots of people who like Java (or whatever language you don't like) and they're happy with that. It does the job, it gets them what they want. Rust perhaps does not and never will. It's not our job to tell people they need to stop doing whatever they're doing unless what they're doing is causing trouble for everyone else, which you'll have a hard time arguing is the case here (a better case, I think, could be to convince people to stop using C and C++ because of the real dangers of lacking memory safety?! But I am sure lots of C user would respond the same way I am doing here about Java :D).
- protomolecule 2y ago>f(g(x)); >Exceptions make this (and similar) patterns suffer so badly. Why one would want to pass an error as an input to a function that expects valid data?
- Expurple 2y agoThat's the thing! In this example, `f` doesn't expect valid data, by design. Some functions are responsible for figuring out what to do in case of unsuccessful operations. Maybe `f` would be easier to imagine as a simple mechanical refactoring where you extract a large `try-catch` block into a separate generic helper function. It's more convenient and abstract to make it accept a single `Result` value, rather than the entire list of arguments to call `g`. Sometimes `f` is not supposed to be responsible for calling `g` or even know about `g` and its arguments.
- protomolecule 2y agoBut why design such functions? And when you do need it, isn't 'the error' just a case of valid data that shouldn't be thrown as exception at all?
- Expurple 2y ago> isn't 'the error' just a case of valid data that shouldn't be thrown as exception at all? That's the thing! Errors are just a case of return data! To me, it just makes more sense to model them like that rather than like checked exceptions. Checked exceptions are simply unnecessary if the language has sum types, Result and some syntax like Rust's `?` for propagating errors easily like exceptions. Checked exceptions are a weird, special and sometimes poorly supported [1] way to "augment the return type". It's easier to just have a single, all-encompassing, composable return type. [1] https://home.expurple.me/posts/rust-solves-the-issues-with-exceptions/#fnref:3 https://home.expurple.me/posts/rust-solves-the-issues-with-e...
- protomolecule 2y ago