3 ms·
>The caller should make this decision explicitly. Isn't it too low-level? Why not define instead cleanup/recovery through RAII and occasional try/catch for mai
by protomolecule 2y ago
>The caller should make this decision explicitly.
Isn't it too low-level? Why not define instead cleanup/recovery through RAII and occasional try/catch for maintaining tricky invariants, and have no need to manually propagate errors?
>`.map_err(Wrapper)?` is way more convenient than catching and throwing a wrapper
If you only wrap exceptions on the API boundary it might be more ergonomic than having to manually propagate errors inside the API implementation and map them anyway.
>you're in the middle of modifying it and currently it doesn't hold its own invariants because you haven't finished restoring those yet
Sometimes it's better to call throwing functions and get needed data before modifying the private data, sometimes it's time to use try/catch. Is it happening often?
>Exceptions make the other error handling patterns too unergonomic
But so does packing valid data and errors together. Imagine you need to call f(h1()) and f(h2()) where h1() and h2() return the same data type for valid data but different types of errors. What do you do?
>the post lacks a concrete example
Yes, having a specific example seems to me to be the best way to discuss design tradeoffs, but you did the second best thing -- you started with declaring your assumptions (exceptions are disguised return values and optimizing ergonomics of passing errors through f(g(x)) is important) with both of which I happen to disagree)
- Expurple 2y ago> Sometimes it's better to call throwing functions and get needed data before modifying the private data Haha! Here we go back to the topic of unchecked exceptions and "the lesser evil". With unchecked exceptions, every function is throwing! Even if it shouldn't throw today according to the docs: 1. The docs could be simply wrong. 2. It isn't a semver breaking change to start throwing in a future version. The compiler won't warn you when this happens. You won't manually audit all your dependencies when updating them [1]. We come back to the need to properly reflect errors in the type system. Without returning them by value, the only popular alternative is checked exceptions. But, as I mentioned in the updated version of the post [2], checked exceptions in Java are unfriendly to generic code. This is important and will come up right now: > Imagine you need to call f(h1()) and f(h2()) where h1() and h2() return the same data type for valid data but different types of errors. What do you do? This depends on the context. A general answer: `f` can be generic over the error type. fn f<E>(result: Result<_, E>) If it needs to do something more specific with E, like logging it, it can also have a more specific bound on E: use std::fmt::Display; fn f<E: Display>(result: Result<_, E>) If it has to be a concrete function, it can use runtime polymorphism instead: use std::fmt::Display; fn f(result: Result<_, Box<dyn Display>>) // in this case, the caller has to explicitly box the error f(h1().map_err(Box::new)) --- [1] Unless you operate on a really serious level where you should strive to use a more reliable language anyway. [2] https://home.expurple.me/posts/rust-solves-the-issues-with-exceptions/#checked-exceptions-in-java https://home.expurple.me/posts/rust-solves-the-issues-with-e...
- protomolecule 2y ago>every function is throwing It was a poor choice of words. You can replace 'throwing functions' with 'throwing code'. There is plenty of code that is guaranteed not to throw -- starting from operations on basic types and ending with noexcept functions if we are talking about C++. >The compiler won't warn you when this happens. It will if you write an asserting in the code that relies on a function not throwing: static_assert(noexcept(f())); >`f` can be generic over the error type Isn't it too much clutter? What if a function has three arguments? You'd need three generic parameters for their errors.
- Expurple 2y ago> There is plenty of code that is guaranteed not to throw -- starting from operations on basic types This is fair, although this places some mental load on the programmer because they need to be careful when dealing with generics / operator overloading or when extracting helper functions. > noexcept functions if we are talking about C++ This is a good C++-specific guarantee. But it's probably outweighted by the C++-specific lack of guarantees. "Operations on basic types" can now invoke UB, if we are talking about C++. > Isn't it too much clutter? In cases with one generic parameter, like the ones I demonstrated, I think it looks ok. Normal generic code. Rust generics are only going to require less ceremony in the future, because the team keeps making progress on implied bounds, lifetime elision, 2024 lifetime capture rules. > What if a function has three arguments? You'd need three generic parameters for their errors. This really depends on the context and what the code actually does, and needs a concrete example to discuss. I don't remember seeing a real world example with three Result parameters, nevermind having generic but distinct errors in them.
- protomolecule 2y ago>careful when dealing with generics / operator overloading In C++, you can't overload operators for built-in types and template parameters are normally constrained: auto f(std::integral auto x) noexcept { return x+x; } But yes, writing C++ templates that work with wide range of types takes some thinking and operator overloading adds to the mental load (or reduces it, when used sparingly and properly). >"Operations on basic types" can now invoke UB They always could. I don't want this conversation to become about Rust vs. C++, I'd gladly use Rust when where is appropriate task. >needs a concrete example to discuss Write a new post when you come up with a bunch of good examples)