rfcs
rfcs copied to clipboard
Extend `&pattern` to general `Deref` implementation.
For example, suppose X
derefs, then &pat
takes X
and dereferences it. If the dereferenced value matches pattern pat
, it matches.
In other words, it extends &pattern
from only allowing dereferencing of &T
to allowing dereferencing of arbitrary T: Deref
.
Alternative is of course to introduce a seperate syntax for it.
Use case or motivating example?
Aside from let &x = Box::new(3);
I'm having a hard time picturing when it would even be possible to use this; Most deref coercions I see in daily use have unsized target types, making them ineligible for this.
@ExpHP just today, I had the case where I had a RAII type derefing to an enum, which I wanted to match against. The code I ended up with was a clusterfuck, because I needed to add another ident for matching against the enum, which is really annoying. If I could just however use &
it would mean, that I could just use one big match
, which is significantly simpler and nicer.
Is this the same as box
patterns, in the case of Box<T>
? https://github.com/rust-lang/rust/issues/29641
This would make pattern matching be able to run arbitrary code, which wasn’t the case before. (For example, struct
matching is structural and does not use PartialEq
.) I don’t know if this is a reason not to do this, but it should be considered.
See also https://github.com/rust-lang/rfcs/blob/master/text/0469-feature-gate-box-patterns.md and https://github.com/rust-lang/rfcs/pull/462.
This would make pattern matching be able to run arbitrary code, which wasn’t the case before.
I'm not certain I see the difference from what's possible today. (Playground)
struct BigRedButton;
impl ::std::ops::Deref for BigRedButton {
type Target = str;
fn deref(&self) -> &Self::Target {
panic!("launching nukes");
}
}
fn main() {
// Nothing to see here, just a pattern match.
let _:&str = &BigRedButton; // thread 'main' panicked at 'launching nukes'
}
That’s deref coercion happening on the right-hand-side of the assignment, not pattern matching. But yeah, maybe deref patterns are no big deal.
Still, because side effects are be involved we’d need to define the order of pattern "evaluation".
Apologies for the noise; after much faffing about I finally see what the difference is.
My mental model of when deref coercions are applied was a bit "off." Apparently they apply to every expression, recursively (AST-wise), but only on the outermost type.
fn main() {
let red = BigRedButton;
let borrow: &BigRedButton = &red;
let _:&str = borrow; // compiles
let _:Option<&str> = Some(borrow); // compiles
let _:(&str, &str) = (borrow, borrow); // compiles
let tup: (&BigRedButton,) = (borrow,);
let _: (&str,) = (tup.0,); // compiles
let _: (&str,) = tup; // AHAH! Finally, a type error.
}
One could say that the examples that compile are exactly those examples where it is possible to insert "as &str" somewhere on the RHS to make the coercions explicit.
Pattern matching on recursive enums.
Currently, this is impossible without #![feature(box_patterns)]
or recursive matches. Recursive matches are just ugly as heck and it's going to be deferenced anyway. #![feature(box_patterns)]
locks your code behind a nightly feature gate and doesn't even work for things other than Boxes. What if you wanted to pattern match on Strings or Rcs? The file I have open right now has three nested matches: one on the enum, one on the boxed value, and another on the String contained in the boxed value. That was before I decided to use box_patterns anyway.
You could add a unsafe DerefTrusted
trait to force only 'trusted' deref implementations to run that don't panic or have side effects. Most smart pointers already require unsafe code anyways, and that's the primary use case for these situations (Box, Rc, String). However it would help ensure that both safe and unsafe code isn't unexpectedly broken by matching bad deref implementations.
However, I still feel that that restriction is unessicarry, since we already allow arbitrary code to run in the general case. Personally, I'm often anoyed by the lack of a general solution and the fact that box
is given special treatment.
It looks like this issue was initially suggested by @nikomatsakis in https://github.com/rust-lang/rfcs/pull/2005#issuecomment-303152861 . Some more discussion occurred around this comment (it's a little hard to follow because the thread is intermingled with discussion of a move
pattern keyword that's unrelated).
Is there any momentum here? What needs to happen next, a formal RFC written up?
match_default_bindings
is now stabilized. I've looked around and it seems that here is where discussion has moved, but it doesn't look like there is much momentum.
Quick summary of where we are at: Motivating example: as @boomshroom mentions, matching on recursive enums. We would typically want this to compile:
let x: Option<Rc<Option<usize>>> = Some(Rc::new(Some(4)));
match &x {
Some(Some(_)) => {},
_ => {},
}
Currently, this works with plain references:
let x: Option<&Option<usize>> = Some(&Some(4));
match &x {
Some(Some(_)) => {},
_ => {},
}
And with Box
es, with the box_patterns
feature and special syntax:
let x: Option<Box<Option<usize>>> = Some(Box::new(Some(4)));
match &x {
Some(box Some(_)) => {},
_ => {},
}
The idea would be to extend the current behaviour of match_default_bindings
, to something like https://github.com/rust-lang/rfcs/pull/1944. Currently, matching automatically derefs plain references as needed. We would like to extend this behaviour to all types implementing Deref
. Another way to phrase this would be to allow &pattern
patterns to match values behind any smart pointer and not just plain references. This would effectively obsolete the box_patterns
feature. Another option would be to use special syntax like box_patterns
, but still extend it to all Deref
types.
This feature has been mentioned in the discussion around match_default_bindings
, but rapidly put to the side. It also has been discussed in the match ergonomics RFC, but a lot has changed since then. Now that match_default_bindings
is stabilized, we'd like to reopen discussion.
The main objections seem to be:
- it would run abritrary code when matching
- it could be too magic and thus confusing and/or lead to subtle bugs (this would be less of a problem with special syntax)
I think that when I made the comment, I was thinking of matching on something like
enum Expr {
App(Box<Expr>, Box<Expr>),
...
}
but you example works just as well. One note about deprecating box_patterns
is that Box
is still special in that it's the only type capable of moving out of a dereference, so match e { Foo(box x) => x }
wouldn't work without box_patterns
unless x
's type is Copy
.
Matching a deep String is another use-case:
enum X {
A(String),
// ...
}
fn main() {
match X::A("test".to_string()) {
X::A(&"test") => (),
X::A(_) => ()
}
}
Need to match deep strings so badly...
@AlbertMarashi and everybody interested! I implemented deref patterns (including strings) in proc macro crate: https://crates.io/crates/match_deref