My apologies if this offended. I really don't think the point I was trying to make was adequately made with this rather loose and clumsy analogy. It sounded OK at the time I wrote it, but, now re-reading it, I think my point got lost. The point I was ineffectively attempting to make is that those who are initially or still opposed to a particular ergonomic initiative are probably, at least initially, doubly-bewildered by any suggestion of removing the original thing that the ergonomic initiative is what they (at least perceive to) believe is simply syntactic-sugar for. This results in a feeling of being put on the defensive and makes it difficult to discuss the matter fully objectively. It's just something to consider when explaining the rationale for these kinds of proposals.
Interesting example! That said, the fact that the original version gives you an error is basically a bug in the borrow checker that I hope we can correct in Polonius. Kind of surprising to me that the ref mut
variant works, actually, though I think I know why that comes about.
UPDATE: To be clear, when I say "bug", I mean "it's a known limitation of the borrow checker that I hope we can lift in the future".
This is true of a large portion of Rust's syntax, because Rust employs both type inference and type-based dispatch:
- Methods and field accesses
- for loops
- Every overloadable operator (including most bin-ops, dereferences, ?, and await)
- Generic function calls
Probably there are more. Basically every construct but the boolean constructs is type-overloaded. Match ergonomics is constrained to operations on reference types, unlike most of these which can be overloaded in the manner of arbitrary types.
I agree. My point wasn't that this should be avoided at all costs. I was pointing out the rather glaring difference between match ergonomics and ?
, for example.
Granted, I don't really view ?
as 'doing different things', because it simply does:
match <expr> {
Ok(v) => v,
Err(e) => return Err(e.into()),
}
The fact that Into
does different things on different types or that match
works on different types doesn't really mean that ?
does different things.
Anyways, I wasn't attempting to make a generalized point. Sometimes features like type inference are very useful and sometimes they aren't worth the tradeoffs. We probably disagree about whether this is true for match ergonomics, but that's fine. I understand that it's nuanced and tradeoffs are always made, regardless.
That's not the desugaring of ?
- it goes through the Try
trait as well as Into
, doing different things based on the type of the operand. (This is how it supports Option
, for example.)
Also, the desugarring using From
not Into
, and this won't be changed because it causes too many inference failures.
Do what?
Describe code style as an actual illness that affects real people. It's also not really correct about said illness, and perpetuates that incorrectness. It's inappropriate for a number of reasons.
I knew this, I've looked at the compiler code that implements it before. My point wasn't to be pedantic about the specific desugaring but to point out that it has a single fixed desugaring.
But that's just it. It doesn't have a single fixed desugaring any more than +
does. Even ignoring error conversion, it invokes Try::into_result
on its operand before the match- the only reason it even includes a fixed match
is that into_result
can't do a non-local return on its own.
So going back to your original claim, it also "allows the same syntax to have multiple meanings based on the declaration of a non-local type," which can decide completely arbitrarily when to return early and what to "unwrap" to.
I read boats' post not as quibbling about ?
, but as emphasizing that binding modes in fact do far less than what you claimed. They give identifier patterns one of a fixed set of two meanings- by-reference or by-value, determined by how the local pattern corresponds to the type of the scrutinee. This is even more limited than autoderef, which can use Deref
impls- there is no open set of possible meanings, not even one locked behind an unstable trait like ?
.
Your other point about giving existing syntax new semantics is also arguably missing the point made subsequently by boats. Lifetime elision takes existing syntax which used to be an error because of a type definition elsewhere and gives it a "reasonable" meaning. The same is true of field projection and method call syntax, thanks to autoderef. Binding modes similarly take an existing syntax which used to be an error, again because of a type definition elsewhere, and give it a "reasonable" meaning.
That is, with or without binding modes, non-local changes to the types in question can change the meaning of a pattern, without necessarily producing a compile-time error. The same is true but to an even greater degree of ?
, lifetime elision, autoderef, and everything else boats mentioned. This is not a new problem unique to (or even especially pronounced for) binding modes, which is why we already had ways to mitigate it, like limiting the reach of type inference.
I think we're talking past each other a bit. The ?
operator doesn't do type inference, the fact that it desugars into a From
does. Basically, ?
can be implemented as a macro_rules
macro, which means that it's syntax is ultimately transformed into precisely one set of tokens implemented in terms of other language features. Those features may have inference or other implicit behaviors, but ?
does not. Hopefully that explains the disconnect.
@ahmedcharles same is true for +
, which simply desugars to a call to Add::add
. A macro_rules!
macro could do that, too. (I got the impression you are arguing that ?
is different? Maybe I misunderstood.)
Exactly! As far as I can tell, c
and d
are calculated with equivalent semantics:
fn main() {
let a = 1;
let b = 2;
let c = a + b;
println!("{}", c);
let d = std::ops::Add::add(a, b);
println!("{}", d);
}
The fact that traits use type based dispatch and that type based dispatch may or may not involve locally visible types at the point of the function call, doesn't mean that +
does anything more complicated than desugar into a fixed usage of other language features. Same with ?
. Match ergonomics however, doesn't share this property.
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.
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").
FWIW, while this is true in general, primitives actually go the other way, where the trait is implemented with just the bare operator.
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.
(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.
This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.