A `Match` trait that allows types like String or Vec to be used in match clauses

The idea is that you'd be able to implement Match<U> for type T and then you'd be able to include items of type U within match statements.

For example, I could implement Match<&str> for String and now, I can use &strs when matching String.

impl Match<&str> for String {
  pub fn match(&self, value: &str) -> bool {
    self.as_str() == value
  }
}

// This is kind of a poor example since here you can just do my_string.as_str() at the top level,
// but you cannot do this when matching, for example, a struct/enum that might have a String or a Vec nested within it.

let my_value = String::from("Hello world!");

// We get this...
match my_string {
  "Hello world!" => todo!(),
  _ => todo!()
}

// Instead of this...
match my_string {
  value if value.as_str() == "Hello world!" => todo!(),
  _ => todo!()
}

// This can also let us match nested Vecs
impl<const N: usize, T>  Match<[T; N]> for Vec<T> {
  // implementation here
}

match my_vec {
  [1, 2, 3] => {}
  // Here, Match is implemented for both [i32; 2] and [i32; 3], meaning both can be matched
  // even within the same statement
  [1, 2] => {}
  _ => todo!()
}

This trait would fix one of the greatest weaknesses of Rust's pattern matching system, but there are a few considerations here.

Matching does not work based on Rust values! Matching works based on patterns, which are just interpreted differently to Rust values. |, when used on a Rust &str, does not work -- but when used in the context of pattern matching, that means match either this, or that. This means that the Match<T> trait signature will probably not be Match<T> because T is the type of a Rust value, and not a pattern matching token.

Perhaps the Match<T> trait could use a new const-generic enum like below, instead of a type:

pub enum MatchToken<T> {
  Str(String),
  Num(f64),
  Array(Vec<MatchToken>),
  // etc . . .
}

However, with this approach you forgo the ability to implement Match for other types like enums or structs.

Perhaps there is a better way to express this idea that simply has gone over my head, or some fatal flaw, but I still believe that it is important for Rust to implement basic matching for its std/alloc library types, and perhaps the best way to do this is by creating a generic interface that allows for extensible Rust matching (since its lack of extensibility is what got us here in the first place).

You could get a long way towards this by including a way to pattern-match through Deref or DerefMut for reference bindings. In addition to String and Vec, this would also make dealing with embedded Arcs, MutexGuards, and the like much easier.

This doesn't feel like an abuse of the Deref trait to me, as its entire purpose is to make custom types behave like references in various situations.

3 Likes

There is a limited form of deref-patterns available on nightly:

#![feature(box_patterns)]

match Some(Box::new("Rust™")) {
    Some(box "Rust™") => {}
    _ => {}
}

It currently only works on Box, because Deref is not a strong enough trait for it. IIRC it needs guarantees that the Deref impl is stable and pure, otherwise you run into issues around proving exhaustiveness of matches

struct Illegal(Cell<bool>);

impl Deref for Illegal {
  type Target = bool;
  fn deref(&self) -> &bool {
    if self.0.replace(self.0.get().not()) {
      const { &true }
    } else {
      const { &false }
    }
  }
}

match Illegal(Cell::new(false)) {
  box true | box false => {}
  _ => unreachable!()
}

This could reach the unreachable!() because the first time the value is derefed it returns &false which doesn't match, then the second time it returns &true which also doesn't match. It'd be preferable if the box pattern could trust the value returns the same thing every time and the box true | box false pattern could be optimised to box (true | false).

5 Likes

I wonder if it would be feasible to define the semantics of match, etc. to call Deref/Mut at most once for any given subexpression, and then operate on the cached result. Unfortunately, that might require some hard-to-understand restrictions in order to maintain the aliasing invariants for &mut.


In your example, that would mean that box true | box false is equivalent to box (true | false) by definition, and also that this only flips the Illegal's state once:

match Illegal(Cell::new(false)) {
    box true => 7,
    box false => 9,
}
1 Like

It's currently under discussion, and it's complicated. The main issue is that match can be semantically understood as inorder trial evaluation of the patterns, and pattern matching can actually already have side effects via the presence of if guards. Calling user-defined Deref impls is another way of getting side effects out of pattern evaluation, so we need to lock down the exact rules before enabling deref patterns.

I'm personally advocating for the "wide" evaluation order and guaranteeing that a deref is called at most once per scrutinee-path. I'm arguing that forbidding the cases where such a single-deref lowering isn't clearly possible is both possible to do in a way explainable in the surface syntax (the restrictions translate to borrowck errors) and isn't overly restrictive. Unfortunately, there might be some "fun" issues still even just with if guards[1].

Handing exhautiveness for deref patterns safely is simple enough, actually, even in the face of interior mutability and multiple derefs. There'll probably be an opt-in trait for the ability to deref in patterns (a "DerefPure") to declare intent to not do shenanigans that would invalidate exhautiveness checking, and then it's as simple in theory as inserting a panicking catchall arm, similar to how out of bounds indexing panics.


  1. Because of how both proposed memory models handle shared mutability (UnsafeCell), the "not immutable" quality is "infectious" at least between variants of an enum, and thus it's possible to pattern match on memory which is not UB to mutate behind the shared reference provided to patterns' if guards, if one variant is pattern matchable and another is shared mutable. This may force "inorder" pattern evaluation to be the semantic and "wide" evaluation to be an optimization only available when there isn't any possibility of shared mutability. ↩︎

1 Like

:bulb: :boom: I have an Idea

  1. to add Traits FromDeref / FromDerefMut, which must satisfy next condition:
  • value == value.from_deref().deref()
pub trait FromDeref<T>: Sized {
    fn from_deref(self) -> T;
}
  1. in matching box true | box false => {} do not check true == cell.deref(), but do vice-versa: true.from_deref() == cell. This is limited to some specific, non variable cases (

Sure, with FromDeref we still could rich unreachable! in case if T::new(value) != value.from_deref()

Pattern matching doesn't use ==; it uses a separate process of structural matching/equality. Additionally, while that would handle equivalence matching, it wouldn't handle anything which actually binds behind the deref, e.g. Some(box x) => dbg!(x) for that example scrutinee.

3 Likes

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