Is `ref` more powerful than match ergonomics?

Except that match ergonomics can be expressed through the trait system as well. [playground]

#[derive(Clone)]
enum Either<L, R> {
    Left(L),
    Right(R),
}

trait Match {
    type Left;
    fn is_left(&self) -> bool;
    fn unwrap_left(self) -> Self::Left;

    type Right;
    fn is_right(&self) -> bool;
    fn unwrap_right(self) -> Self::Right;
}

impl<'a, L, R> Match for &'a Either<L, R> {
    type Left = &'a L;
    type Right = &'a R;
    ..
}
impl<L, R> Match for Either<L, R> {
    type Left = L;
    type Right = R;
    ..
}

macro_rules! d {
    ($d:expr) => {{
        let d = $d;
        println!("{}: {} = {}",
            stringify!($d),
            if std::mem::size_of_val(&d) > 1 { "&u8" } else { "u8" },
            d
        )
    }}
}

fn main() {
    let either = Either::<u8, u8>::Left(Default::default());
    
    // the dedicated syntax
    match &either {
        Left(l) => d!(l),
        Right(r) => d!(r),
    }
    
    // is ~equivalent to this trait dispatch
    {
        let temp = &either;
        if temp.is_left() {
            let l = temp.unwrap_left();
            d!(l);
        } else if temp.is_right() {
            let r = temp.unwrap_right();
            d!(r);
        } else {
            unreachable!()
        }
    }
    
    // the dedicated syntax
    match either.clone() {
        Left(l) => d!(l),
        Right(r) => d!(r),
    }
    
    // is ~equivalent to this trait dispaatch
    {
        let temp = either.clone();
        if temp.is_left() {
            let l = temp.unwrap_left();
            d!(l);
        } else if temp.is_right() {
            let r = temp.unwrap_right();
            d!(r);
        } else {
            unreachable!()
        }
    }
}

Sure, there isn't a user-visible trait involved, but that's a) because that trait would be unique for every type, b) said functionality would explode quickly with non-default binding modes, and c) because the language doesn't allow you to customize match behavior such that it can do smart optimizations around matching and deconstructing types. But behavior dispatch is ultimately the same as if it were handled by trait dispatch.

3 Likes

In fact, if we didn't have ref or & binding modes, I could explain match solely in terms of discriminant and unsafe fn unsafe_unwrap_variant(self) -> Variant dispatched like the trait above, adding in unsafe fn unsafe_variant(&self) -> &Variant for if guards.

It's only when you add in the ref binding mode (not default binding modes!) that you lose the ability to discuss match like a trait dispatch, and have to give it different "smart" semantics.

Specifically,

match either {
    Left(ref l) => {},
    Right(ref r) => {},
}

doesn't move from either. There is no way to express that match $expr may or may not move from $expr depending on the exact patterns used, depending on if they all use ref or _ to not move from the place. (Keep in mind the requirement of only evaluating $expr once.)

So to directly counter your argument, @ahmedcharles, I agree with @matklad and conclude that match with default binding modes and not an explicit ref binding mode is simpler and more consistent with how it works than one with a ref binding mode (... modulo that _ still doesn't move out of the place, even when matching by "move").

3 Likes

FWIW, while this is true in general, primitives actually go the other way, where the trait is implemented with just the bare operator.

1 Like

Can you explain why that is an important difference? Type based dispatch of traits is a very complicated and totally non-local system. There's even actually inconsistencies between the behavior of binary operators and . method lookup (so that a.add(b) sometimes will compile when a + b would not). Its honestly a bit of a mess when you look at it too closely.

As far as I can tell, the behavior of match ergonomics is far more constrained and far simpler to understand than this system. Basically I think we all accept the complexity of this system as a sunk cost, but I'd say its certainly the most complex instance of non-local reasoning in Rust by a large margin.

What I kind of understand as an argument is the idea that maybe type based dispatch is so confusing that Rust can only have one form of type based dispatch. So any type-based dispatch which isn't sugar for this trait dispatch is unacceptable. Is that your opinion? I at least understand that, even if I don't agree.

But that's very different from the argument I responded to, when you said that the problem was that semantics were determined by the value of inferred types, a feature of trait based dispatch and anything which desugars to it.

2 Likes

(I am aware, but this is an unobservable implementation detail of rustc: a macro desugaring + to Add::add does have the correct behavior.)

But it is observable through constness: traits cannot be used in const while primitive addition can be.

5 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.