Pre-RFC: Anonymous variant types

Guilty as charged!

1 Like

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(),
    }
}
1 Like

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.

1 Like

Sorry for my poor English, I’m not native speaker :wink:

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),
        }
    }
}
1 Like

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.

4 Likes

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.)

1 Like

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.

4 Likes

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.

3 Likes

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.

1 Like

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.]]

1 Like

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?