Was this idea for struct/enum unification considered?


#1

I think this idea has some elegance. Had it been considered?

All variants of an enum should be pre-defined structs. That is, Option<T> would be defined like this:

struct Some<T>(T);
struct None;
enum Option<T> { Some<T> | None }

#2

What do you think the benefit of this is? I see no elegance, only introduction of complexity. (Note that the following is only speculation; I have no idea what you have considered, or if you have considered the implications to the type system in depth.) In particular, Some(32i) would have multiple possible types: Some<T>, Option<T>, and whatever other enums might happen to include it. On top of that, None is now mising a type parameter. How do you specify which T the None should be for?


#3

Ok, it just came to my mind and I thought it was interesting and asked if anyone had already considered it in depth. It’s less explicit and I see no clear use case, so it’s probably no good. Thanks for the answer!


#4

This involves a rather significant reworking of the type system, and it happens to be one I have thought about quite a lot, ever since #8277 about anonymous sum types.

Basically what this idea of yours boils down to is making variants first-class types and replacing enums with what is much closer to type unions (though discriminated, of course).

I have a feeling that the end result of something like this could be superior, though it’s certainly not without its difficulties (most notably, what happens if you take the union of two types with a common variant?) but it would be a very drastic change and is, I think, too late for Rust.

I would take it a little further and formulate your example somewhat like this:

type Some<T>(T);
type None;
type Option<T> = Some<T> | None;

By that point you might as well throw away None and define it thus:

type Option<T> = T | ();

(“What about when T is ()?” you ask. Indeed, and I don’t know the answer yet. Probably it would be forbidden.)

Such a language as this would then have genuine subtyping; T would be a subtype of Option<T>, for example, and so inside your method you would be able to implicitly cast a T to a T | ().

This sort of thing would be particularly useful for things like remote APIs:

// Allowing inline type definitions like this is half-baked sugar
// for these things and has not been thought through at all.
type ErrorCode = type BadThing = 1
               | type AnotherBadThing = 2;

type Message = type Foo
             | type Bar(int)
             | type Baz {
                   happy: bool,
               };

type ApiError {
    detail: String,
    code: ErrorCode,
}

fn call() -> Result<Message, IoError | json::Error | ApiError>;

And because of the subtyping relationship where an IoError can be implicitly cast to IoError | json::Error | ApiError (it will do Magic with regards to the discriminants, of course), try!(stream.read(buf)) would work. (As it is, it’s distinctly unpleasant with lots of boilerplate, and you can’t use try!.)

Well, these are interesting things to think about, but I don’t think Rust will be getting it.


#5

Thanks! That’s very interesting. Nice to hear that my thought wasn’t entirely stupid!


#6

As I understand it, RFC PR 142 uses a variant (no pun intended) of this in its design. The main difference is that sub-structs of enums don’t need to be declared beforehand, so in the following enum declaration:

enum Foo {
    Bar(int),
    Baz(int),
}

Bar and Baz are substructs of Foo.


#7

My 50 cents. I’ve thinking about this kind of enum discriminants, when trying to design simple 3d primitives library. What could be done with types as discriminants:

let l1 = Line::new();
let l2 = Line::new();
mathch intersect!(l1, l2) {
    Point(p) =>  println!("lines intersect in {}, p),
    None => println!("no intersection")
};   

let l3 = Line::new();
let p1 = Plane::new();
match intersect!(l3, p1) {
    Line(_) => println!("line is contained by plane"),
    Point(p) => println!("line intersect plane in point {}", p),
    None => println!("line is parallel with plane)
};

Note that Point is common discriminant for result of line-to-line intersection and line-to-plane intersection. Now it is possible to achive that with namespacing by modules: line_to_line::Point; line_to_plane::Point, which is basically unbearable.

With such composition more interesting scenarios can emerge, like Chris shown:

fn intersects<T1, T2>(p1: &T1, p2: &T2) -> bool {
    match intersection!(p1, p2) {
        None => false,
        _ => true
    }
}

So there are some uses for this style of enums, possibly still worth considering.