5 ms·
Too bad Rust doesn't have union types (aka adhoc / anonymous unions) yet. Without them, using typed errors is very clumsy. Optimally, you would write the follo
by valenterry 4y ago
Too bad Rust doesn't have union types (aka adhoc / anonymous unions) yet.
Without them, using typed errors is very clumsy. Optimally, you would write the following code:
fn foo(r1: Result<i32, Error1>, r: Result<i32, Error2>) {
let i1 = r1?;
let i2 = r2?;
// ...
}
and Rust would infer the return type to be Result<String, Error1 | Error2>
without having to do any extra definitions or conversions.
- sinistersnare 4y agoI think that taking options and results usually is an anti pattern. You want to handle the errors at creation-site and not propagate it to another function. Of course this is just a minimal example... Others have talked about the real point of this comment, and it's just a minor nit. I wanted to point this out for new rust programmers reading this.
- yakubin 4y ago„Yet”? Are there any plans to add them?
- kibwen 4y agoThere are not. I don't believe they've ever been thoroughly proposed.
- insanitybit 4y agoAnonymous sum types are something I want for error handling as well. In practice though I'm not sure it would really make my life that much better.
- masklinn 4y agoIt would make life a bit terser, but I’d rather have polymorphic variants. And maybe only anonymous enums over polymorphic variants. That would make precise error handling on libraries quite a bit better.
- valenterry 4y agoNot sum types. Those are union types. The difference is important, since if you work with two results (or two functions that return results) that use the same error-type you most often don't want to end up with a tuple of two times the same error but simply A<String, Error>. Of course, if you care about which error is from which function, you can always easily do that by wrapping them into a sumtype, but in practice this is a rather rare use-case in application code at least.
- insanitybit 4y agoUnions don't have a discriminant. Anonymous Sum types have a discriminant, you just can't name it. Unions in Rust are unsafe because you can't tell what the underlying value will be.
- valenterry 4y agoWell, that depends. If the union consists of two types that share the same underlying structure, then obviously at runtime we can never know what the value is. But otherwise we can. And this is something that we will know at compile-time, so we can prevent runtime-checks that would not work.
- insanitybit 4y agoI must not be understanding what you're asking for because it sounds like anonymous sum types.
- valenterry 4y agoMaybe this is just about terminology. But essentially, when it comes to union types, they behave like sets. The compiler merges them. (A | B) | (A | B) is the same as A | B. But for sum types (even anonymous ones such as tuples) the compiler can't merge them because that would lose information (if the result is from the first A | B or the second one). Instead, you end up with a nested structure. Which one is desired depends on the use-case, but it's definitely different.
- jamincan 4y agoThe library ecosystem does fill that gap somewhat with crates like anyhow [1] which also allow you to add context to the errors you return. 1. https://docs.rs/anyhow/latest/anyhow/ https://docs.rs/anyhow/latest/anyhow/
- valenterry 4y agoUnfortunately this does not solve the problem. If you use this, you lose typesafety and the documentation you get through the (inferred) types. I.e. with union types you can see at a glance in your IDE which types of errors a function can return - this is too valuable to give it up.
- zaarn 4y agoYou don't really loose type safety, rather it moves to runtime. Downcasting an anyhow error to the concrete internal type is certainly possible and won't panic in my experience.
- valenterry 4y ago"moves to runtime" exactly means to lose typesafety though.
- martijnarts 4y agoFor anyone interested in what this would look like in Rust now, there's two ways. For libraries, people tend to recommend the thiserror crate. Code sample[0]: #[derive(thiserror::Error, Debug)] enum Error { #[error("One")] One(#[from] Error1), #[error("Two")] Two(#[from] Error2), } fn foo(r1: Result<i32, Error1>, r2: Result<i32, Error2>) -> Result<..., Error> { let i1 = r1?; let i2 = r2?; // ... } Whereas for binaries, people usually recommend anyhow. Code sample[1]: fn foo(r1: Result<i32, Error1>, r2: Result<i32, Error2>) -> anyhow::Result<...> { let i1 = r1?; let i2 = r2?; // ... } [0]: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=b6636bd945cfee7312e49a8c8c0a821f https://play.rust-lang.org/?version=stable&mode=debug&editio... [1]: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=55afee0d797e1139e5f90726a5b54d16 https://play.rust-lang.org/?version=stable&mode=debug&editio...
- exDM69 4y agoFor simple tasks you can get away without any external crates by using `Result<T, Box<dyn Error>>`. But it's much more comfortable to use thiserror or anyhow in the long run. fn read_string() -> std::io::Result<String> { Ok("123".to_owned()) } fn main() -> Result<(), Box<dyn std::error::Error>> { let s = read_string()?; // io::Error let n = i32::from_str_radix(&s, 10)?; // num::ParseIntError println!("read number: {n}"); Ok(()) }
- xedrac 4y agoExcept you often need `Result<T, Box<dyn Error + Send + 'static>>` if you go that route. At the very least, you should create a type alias for it. I very much prefer the use of `anyhow` and/or `thiserror` depending on if I need typed errors.
- WirelessGigabit 4y agoAnd the reason it works in anyhow is ... They have those conversions: https://docs.rs/anyhow/latest/anyhow/struct.Error.html#impl-From%3CError%3E-for-Box%3Cdyn%20StdError%20+%20%27static%3E https://docs.rs/anyhow/latest/anyhow/struct.Error.html#impl-...
- tialaramex 4y agoEven if the "Ad hoc union" becomes a thing in Rust, you are not likely to get inference of return types. The return type is part of the function signature and Rust deliberately doesn't infer signatures, in languages with "too much" inference it's impractical for the human programmer to keep track of types because it's all inferred, this has started to be a problem in C++ as more and more things are auto. Rust has some very sophisticated inference inside a function (including partial inference and inferring types from how they're later used), but none for the signature.
- valenterry 4y agoI think it's the job of the IDE to make that work, but I agree, without IDE this can quickly become a problem.
- friendzis 4y agoI think method signatures are part of application/typesystem design and should not be inferred. Explicitly provided types are a feature. Inferred/auto type signatures are "necessariy evil" to reduce boilerplate type declarations around code. While codeblocks `fn1(fn2(), fn3)` and `var r1 = fn2(); var r2 = fn3; fn1(r1, r2)` are more or less identical, unless you have static type definitions for these methods you start having a very bad time inferring what types are being passed around. Consider typical python wrapper library with liberal use of *kwargs to pass non-wrapped arguments down to wrappee. Those arguments and their types (as much as they are available in python, you get the idea) are entirely missing from wrapper code and make changes at call site pretty difficult
- valenterry 4y ago> unless you have static type definitions for these methods you start having a very bad time inferring what types are being passed around. It doesn't matter if the types are annotated explicitly or inferred. The amount of information is 100% the same. The IDE could just fill in the types _exactly_ in the same way as they would look like when annotated by hand - maybe just with a different color. IntelliJ does this quite well, see for instance: https://i.stack.imgur.com/tiqjc.png https://i.stack.imgur.com/tiqjc.png This is not a guess. This is the real types. There is literally no difference in the behaviour/semantics of the code. > Consider typical python wrapper library with liberal No, because python is not statically typed. You can't compare that.
- xavxav 4y agoI know that this is a common wish, but anonymous sum types have pretty catastrophic impacts on type checking and lead to all sorts of bizarre corner cases like the following: let a = if cond { 1 } else { 1.0 }; a + 3 Now, the error would be pushed to the `+` operator because there isn't an `Add` for `f32 | u32`. Granted, this is a trivial example, and a programmer can easily see through it, but in general this can get very overwhelming and cause errors to leave their 'root cause'.
- valenterry 4y ago> Now, the error would be pushed to the `+` operator because there isn't an `Add` for `f32 | u32`. Why not? It makes total sense for one to exist. This is something the language needs to deal with, not the programmer. But the programmer always sprinkle annotations so that errors can only reach so far.
- shakow 4y ago> It makes total sense for one to exist. I disagree: IME, when you add a float and an integer, you want to cast float to integer 50% of the time, and integer to float the remaining 50% of the time. Even if it leads to more verbosity, I prefer arithmetic operations to be endomorphisms and use explicit casts.
- valenterry 4y agoI'm sure you can just use a linter or configure the compiler to error out in such a case if there are really no use-cases, no?
- hn92726819 4y agoWhat do you mean? It already errors out if you try adding them together because it requires explicit casts. What the comment you're replying to is saying is that it's better to just explicitly cast than figure out what the compiler guesses. let x=19/10+0.5 I want to decide for myself if my result is 2.4, 1.5, 1, or 2 in the example above