Idea: `where match` clauses

Hello again! I'm replying here to the comments I got on the Tracking issue for specialization github issue, because I don't want to derail their thread any further :slight_smile:.

@nikomatsakis

Thank you for your interest in the idea, and for linking aturon's blog post! While I worked with the specialization RFC as base for this idea, I wasn't aware of how it fits in the larger picture of inheritance patterns. It is not surprising in the slightest, but this demonstrates that specialization has already been given a lot of thoughts from the rust team.

I tried to adapt the longer example from the blog post to using where match clauses, here's what I got at:

Somewhat long rust code using `where match`
// Node ////////////////////////////////////////////////////////////////////////

trait Node {
    fn parse_plain_attribute(&self, name: &Atom, value: DomString) -> AttrValue where match Self {
        T: Element => {
            match name {
                &atom!("id") => AttrValue::from_atomic(value),
                &atom!("class") => AttrValue::from_serialized_tokenlist(value),
                _ => fallthrough::parse_plain_attribute(self, name, value), // <- This is where we need a way to reference "the next impl"
            }
        } 
        _ => AttrValue::String(value)
    }

    // A very radical alternative that doesn't require fallthrough but is a bit far from the proposed `where match` construct:
    fn parse_plain_attribute(&self, name: &Atom, value: DomString) -> AttrValue {
        match (Self, name) {
            (T: Element, &atom!("id")) => AttrValue::from_atomic(value),
            (T: Element, &atom!("class")) => AttrValue::from_serialized_tokenlist(value),
            (_, _) => AttrValue::String(value),
        }
    }
    // additional virtual methods for Node
}

// non-virtual methods for Node
impl Node {
    fn is_parent_of(&self, child: &Node) -> bool {
        // ...
    }
    // additional methods here
}

// Element /////////////////////////////////////////////////////////////////////

trait Element: Node {
    fn as_activatable(&self) -> Option<&Activatable> where match T {
        T: Activatable => Some(self),
        _ => None
    }
    // additional Element methods
}

// non-virtual methods for Element
impl Element {
    fn nearest_activable_element(&self) -> Option<&Element> {
        // ...
    }
}

// Activatable /////////////////////////////////////////////////////////////////

trait Activatable: Element {
    // moar methods
}

// HtmlAnchorElement ///////////////////////////////////////////////////////////

struct HtmlAnchorElement {
    rel_list: DomTokenList,
    // moar fields
}

impl Node for HtmlAnchorElement {
    fn parse_plain_attribute(&self, name: &Atom, value: DOMString) -> AttrValue {
        match name {
            &atom!("rel") => AttrValue::from_serialized_tokenlist(value),
            _ => default::parse_plain_attribute(self, name, value), // here 'default' works the same without specialization
        }
    }
}

impl Element for HtmlAnchorElement {
    // ...
}

impl Activatable for HtmlAnchorElement {
    // ...
}

// HtmlImageElement ////////////////////////////////////////////////////////////

struct HtmlImageElement {
    url: Option<Url>,
    image: Option<Arc<Image>>,
    // moar fields
}

impl Node for HtmlImageElement {
    fn parse_plain_attribute(&self, name: &Atom, value: DOMString) -> AttrValue {
        match name {
            &atom!("name") => AttrValue::from_atomic(value),
            &atom!("width") | &atom!("height") |
            &atom!("hspace") | &atom!("vspace") => AttrValue::from_u32(value, 0),
            _ => default::parse_plain_attribute(self, name, value),
        }
    }
}

impl Element for HtmlImageElement {
    // ...
}

As you correctly determined, the difficult part is default, aka the ability to refer to "the next" (more generic) implementation. Indeed, in traditional match constructs in Rust, we don't have a way to "fallthrough" the next match arm (which may or may not be applicable depending on bounds in the case of where match) . This is made worse by the pattern of "specialization trait I propose", because in the implementation of the specialization trait, we don't have any explicit link to the base trait. The following example is an adaptation of the blog post's, and demonstrate the problem:

// Crate up

use bar::Bar;

pub trait Foo {
    fn foo(&self);
}

pub trait FooSpec {
    fn foo(&self);
}

impl<T: Bar> Foo for T {
    fn foo(&self) where match T {
        T: FooSpec => FooSpec::foo(self),
        _ => { 
            let bar = self.bar();
            /* do complicated stuff with bar */
        }
    }
}

// Crate down

use up::Foo;
use up::bar::Bar;

pub trait Baz {
    fn baz(&self);
}

impl<T: Bar + Baz> FooSpec for T {
    fn foo(&self) where match T {
        T : FooSpecSpec => FooSpecSpec::foo(self),
        _ => {
            self.baz();
            // Here, we'd like to reference the "generic implementation" of foo that "does complicated stuff".
            // I couldn't find a good option for this, here are some possibilities:
            // Option A: referencing the generic trait in the specialized trait "just works"
            // Very magical, and this is allowed today with another meaning (infinite recursion)
            <T as Foo>::foo(self);
            // Option B: still magical
            <T as default Foo>::foo(self);
            // Option C: explicit the implementation to use, but feels ugly
            <T where T : _ as Foo>
            // Option D: named implementations / conventions :'( or forced ...
            up::foo_base(self);
            // Maybe could do something with a proxy type that doesn't implement FooSpec, too?
        }
    }
}

Ideally, we'd be able to say "I want to call the more general implementation" (that being defined as "the next in the where match that is applicable to T) of Foo from FooSpec, without having to name that implementation. If someone has an idea on how to do this, I'd really like to hear it!

Other than that "thorny" problem, I don't see what existing specialization could do that where match couldn't wrt the larger inheritance story.

@Dark-Legion:

Thank you for finding this idea interesting! I'm open to starting an RFC for where match (maybe in this thread or in a different one as a Pre-RFC first?), but I think I need some advice on the "Motivation" section. To me, the ability to emulate specialization is a strong motivation for the feature. If we don't have this and want where match additionally to specialization, then I think we need another strong motivation for where match. First because it is an increase of syntax surface, second because of @H2CO3's reasonable remark that where match and specialization would be overlapping features, and I think we need a strong motivation before adding multiple ways of doing something. So, while I find where match intellectually pleasing, my main motivation is how it can allow for some form of specialization through the "specialization trait pattern". Do you (or others) see other use cases for this feature? On the github thread, the8472 mentioned a future extension of being able to use where match for matching on const expressions, such as size_of or align_of.

Again, thank for you interest! I may try and start writing a Pre-RFC, at least to specify a bit the bounds of this where match construct, and what a first iteration could be.