Guilty as charged!
Third revision. The primary changes were to explain further my decisions, and to start referring to the variants of anonymous variant types as anonymous variants.
What if implicitly generate imps of From<T>
for each variant, and use it like that:
test() -> i32 | f32 {
(2.3).into()
}
??
And can we use the type names when matching(beside the indices):
fn test(x: i32 | f32) -> i32 {
match x {
i32(v) => v,
f32(v) => v.into(),
}
}
That would probably require making From
a lang item.
I donât understand why this would be different than tuples and we could just implement traits for the first 30 (A | B | ... )
types like we do for the first 30 (A, B, ...)
types. Its not the best solution and we would someday want a general way to implement things for all tuples and anonymous enums but that will still take a while.
Now days, error handing in Rust, we could return Enum or failure::Error.
Obvious, design a big enum which named MyError
is boring, and most of the time we canât design a very perfect MyError
from the beginning. Which means, Mostly, we would using ..
for future error kind, this is obviously encouraging us not to deal with it, but to return it directory.
With failure::Error
, itâs easy to get backtrack, but we couldnât ensure all kind error are handled in compile time.
With anonymous enum, our code could be like this:
fn foo() -> Result<Foo, (Error1 | Error2 | Error3)> {
// ... Some code
}
fn user() {
if let Err(error) = foo() {
match error {
Error1(x) => { /*... Some code */ }
Error2(x) => { /* ... Some code */}
Error3(x) => { /* ... Some code */}
}
}
}
What do you mean by that?
And by that? Isn't propagating the error (assuming you meant "directly") considered handling it? It sounds like you are asserting this is something intrinsically bad.
failure
's custom derive is intended exactly to alleviate that repetitive task.
I don't see how this is improved by making it an anonymous type with anonymous variants. You can add cases to and remove cases from your own enum in your own crate with regular named enums and variants too. You can't change which kind of errors an external crate's functions return even with anonymous variants.
Sorry for my poor English, Iâm not native speaker
Think about this:
pub enum MyError {
Error1(E1),
Error2(E2),
// ... Some code
Error6(E6)
}
/// @author Alice
/// @return Err(Error1) or Err(Erro2)
fn must_hand_all_kind_error() -> Result<!, MyError>{
// ... Some code
}
/// @author Bob
fn foo() {
if let Err(error) = must_hand_all_kind_error() {
match error {
Error1(x) => handle_error1(x),
Error2(x) => handle_error2(x),
_ => unreachable!() // Alice told me, It's unreachable!
}
}
}
One day, Alice added a possible type of error, but forgot to inform Bob:
/// @author Alice
/// @return Err(Error1) or Err(Error2) or Err(Error3)
fn must_hand_all_kind_error() -> Result<!, MyError> {
// ... More code
}
Compiler doesnât know all kind errors must be handled, and pass it, but panic in runtime.
With failure::Error
, there are no different.
Same story for anonymous variant types:
/// @author Alice
fn must_hand_all_kind_error() -> Result<!, (Error1 | Error2 | Error3)> {
// ... More code
}
fn foo() {
if let Err(error) = must_hand_all_kind_error() {
match error { // Uncompilable
Error1(x) => hand_error1(x),
Error2(x) => hand_error2(y),
}
}
}
What is the purpose of a type that has two unnamed variants with identical payload types?
(i64 | () | i64 | (i64, f64))
With this, a function can return a 0(i64)
or a 2(i64)
. How should the caller handle these values? If itâs possible that the caller should handle them in different ways, then using a anonymous variant type would be ill-advised in this case because named variants would be much better. It seems that having this ability encourages bad API design.
One could argue that this issue is the same as with tuples: if a function returns (i64, i64)
, how would the caller know the semantic meaning of each value? However, while itâs bad to use (i64, i64)
in places where the values have semantic difference, itâs OK to use it to represent a pair of values with the same semantic meaning. With variant types, on the other side, there is no good reason to use a (i64 | i64)
type, and forbidding that wonât reduce the usefulness of anonymous variant types.
If no type duplicates were allowed, it would be possible to avoid addressing the variants with numeric identifiers (.0
, .1
, etc.). A possible construction syntax would be _::i64(value)
or even _::_(value)
if the type can be inferred. It would also be possible to match on the value by specifying types, as proposed above. Removing numeric identifiers is a good thing, since position of a variant within the variant type canât have a semantic meaning.
It could come from a generic function, for example
fn pick_one<T, U>(t: T, u: U) -> (T | U) {
if rand_bool() {
_::0(t)
} else {
_::1(u)
}
}
let x: (u32 | u32) = pick_one(10u32, 20u32);
Now this example is trivial, but this idea can be extended to other more complex cases where types could overlap in generic functions.
edit
We could even consider the input
fn combine<T, U, V, W>(a: (T | U), b: (V | W))
-> (T, V | T, W | U, V | U, W) {
match (a, b) {
(_::0(t), _::0(v)) => _::0(t, v),
(_::0(t), _::1(w)) => _::1(t, w),
(_::1(u), _::0(v)) => _::2(u, v),
(_::1(u), _::1(w)) => _::3(u, w),
}
}
let x = combine((u32 | u32)::1(10), (f32, i32)::1(-10));
While in these examples there arenât really any semantic differences, with generic code it is possible that there will be semantic differences between each of the variants but that the variants may end up being the same type in certain cases.
Actually I think it would be nice if a type (A|A)
could be equivalent to A
. This would be perfectly sound the upper example, but I see that there are problems with generic code and âtype-based matchingâ.
Consider for example this:
fn maybe_panic<A, B>(val: (A | B)) {
match val {
a: A => (),
b: B => panic!(),
}
}
let b: u32 = 0;
maybe_panic::<i32, u32>(b); // -> panics
maybe_panic::<u32, u32>(b); // -> not sound
While with the structure based approach there is perfect determinism here:
fn maybe_panic<A, B>(val: (A | B)) {
match val {
_::0(_) => (),
_::1(_) => panic!(),
}
}
let b: u32 = 0;
maybe_panic::<i32, u32>(_::1(b)); // -> panics
maybe_panic::<u32, u32>(_::1(b)); // -> panics
But I find the possibility of having anonymous variants with equal types very unintuitive and I think that it might lead to very unreadable and therefore errorprone code. One Idea out of this would simply to disallow constructing such variant types.
Another idea would be to handle it via bounds on the types of generic parameters. We could add bounds for specifying that A != B
and in cases where this bound is not given a special case where A == B must be constructed in the match statement:
fn maybe_panic<A, B>(val: (A | B)) {
match val {
a: A => (),
b: B => panic!(),
ab: A + B => (),
}
}
fn maybe_panic<A, B>(val: (A | B))
where A != B
{
match val {
a: A => (),
b: B => panic!(),
ab: A + B => (),
}
}
(I know Iâm mixing types with trait bounds syntax here but this is just the first possible syntax I came up with.)
Any more unintuitive than compilation failing because deep somewhere in the middle of a library two different generic type variables which happened to have the same values in a particular monomorphization got put together into an anonymous variant type?
Again, the priority is to be minimal and relieve the work needed for implementation, not to be fully featured. Complexity, ambiguity, and interaction corner cases have killed proposals like these before, so I'm trying to avoid that.
I don't see at all how that is related to anonymous variants. In the sample where you are using enum
, you could just not use unreachable!()
in exactly the same manner you don't use it in your second example. The compiler would then catch the missing enum case in the pattern match just as well as it would in the case of the snippet with anonymous variants. (The converse is also true, you could write unreachable!()
for the 3rd anonymous variant because Alice promised it won't be returned, and then have your program panic in the same way.)
It's bad practice to trust functions to never return a subset of their return type (which is what you are doing when you write "Alice told meâŚ") anyway. If there's an error which you don't handle but which the type signature of the callee permits, just keep propagating it up the call stack (maybe even adding some annotation as to where it came from, or that it shouldn't happen, or something.) This doesn't need anonymous sum types at all, exhaustiveness analysis already works perfectly well for enums
right now.
This still doesn't make sense to me. failure::Error
is a struct
. It doesn't split control flow, you can't match on it, consequently you can't induce the bug you described by using it.
As a method of being forward-compatible with type based variants, perhaps this proposal could forbid naming of anonymous enum types with two structurally equivalent variants.
Though actually, this doesnât prevent construction, though I donât think thatâs something that a type-based unions would be able to guarantee.
fn and_u32<T>() -> (T | u32) { (T | u32)::1(0) }
fn u32_and<T>(it: (u32 | T)) -> (u32 | T) { it }
let mut x = and_u32();
x = u32_and(x);
I believe the point is that introducing a new error enum for every potentially failing operation is a heavy-handed and more effort than they feel is worth it for their application; rather, they either use Box<dyn Error>
(failure::Error
) to say âsomething went wrongâ, or create one large ApplicationFailure
enum, which is any possible failure throughout the application.
Allowing anonymous enums would reduce the friction to create locally applicable sums of potential errors, thus lead to (them writing) more robust code in the face of errors.
If it really forbids naming such a type, then the compiler is required to perform post-instantiation type checking once the type variables are bound to concrete types. I.e., we would be back in C++ template land, where the body and the signature of a function might or might not compile based on the values itâs called with. I would really-really not like to go there.
Or, we could just forbid a generic type variable in an anonymous variant type altogether, although that probably severely limits their usefulness.
Another âsolutionâ would be to actually make them union types instead of sum types (so that duplicates are filtered out), although union types can be very surprising to use and reason about. E.g. a couple of young functional languages with union types define Option<T>
as T union null
, and then Option<Option<T>> == Option<T>
and other ambiguous pain points arise. Needless to say, Iâd rather not do that either.
This wasn't my intention, I meant just to prevent direct construction of such a type. But then I realized generics+type inference defeat such a weak preventative measure.
[[Note to readers: the intent of this RFC is to be a minimal anonymous enum. Valid points would include forward compatibility with your pet semantics, but the minimal proposal is deliberately syntactically sour and limited, to aid in carving out a mostly-agreeable subset.]]
Such a form is just temporary status of refactoring. However it do have its use. Consider
fn test(i: u8) -> impl Debug {
match i {
0 => (i64|()|i64|(i64,f64))::0(10),
_ => (i64|()|i64|(i64,f64))::1(()),
2 => (i64|()|i64|(i64,f64))::2(20),
3 => (i64|()|i64|(i64,f64))::3(30,0.0),
}
}
So it would be really helpful if the Debug
can be implemented automatically.
Shouldnât the wildcard case be at the end?