Match a slice of str or String

I have a function that can take a &[&str] or a &[String] and return different values based on the slice. It works fine using == if I add a PartialEq constraint:

pub fn info1<'a, S>(path: &[S]) -> Option<String>
where
  S: PartialEq<&'a str>,
{
  if path == ["a", "b"] {
    return Some("It's a-b!".to_string())
  }
  None
}

But it doesn't work if I use match:

pub fn info2<'a, S>(path: &[S]) -> Option<String>
where
  S:  PartialEq<&'a str>,
{
  match path {
    ["a", "b"] => Some("It's a b!".to_string()),
    _ => None,
  }
}
error[E0308]: mismatched types
  --> src/lib.rs:16:6
   |
11 | pub fn info2<'a, S>(path: &[S]) -> Option<String>
   |                  - this type parameter
...
15 |   match path {
   |         ---- this expression has type `&[S]`
16 |     ["a", "b"] => Some("It's a b!".to_string()),
   |      ^^^ expected type parameter `S`, found `&str`
   |
   = note: expected type parameter `S`
                   found reference `&'static str`

Is there any way to make it work? Ideally without requiring something like ["a".as_foo(), "b".as_foo()].

You can't match a String against a string slice directly, and you can't do it on the other side of a trait or generic either. Rust's pattern matching mechanism requires you to be able to write out the constructor for the type in the pattern, so you can't match a trait, a generic parameter, or the internal structure of a type with private internals, because the constructor is abstracted away.

Your .as_foo() idea is the best way to do this, because it allows you to convert the type into something you can construct.

Alternatively, you can do something like this, but it does an allocation:


pub fn info1<'a, S>(path: &[S]) -> Option<String>
where
  S: AsRef<str>,
{
  match &path.iter().map(|s| s.as_ref()).collect::<Vec<_>>()[..] {
    ["a", "b"] => Some("It's a b!".to_string()),
    _ => None,
  }
}

Can you explain this a little bit more? I can't find any documentation on what exactly Rust does when you match against something like ["a", _, "b"]. It obviously doesn't desugar to something like

path.len() == 3 && path[0] == "a"  && path[2] == "b"

because then it would work!

[ ] is the Rust syntax for constructing an array or slice, so you can use that syntax in a pattern to match an array's or slice's internals. Likewise, (,) constructs tuples, Enum::Variant ... constructs enums, Struct {...} constructs structs, etc., and you can use those in patterns to match the internals of tuples, enums, etc., respectively. The only way to pattern match a String's internals is to write String { ... }. So you need to write the constructor of the type to match the internal structure of that type.

Essentially, the language knows how to construct these types, so it can 'deconstruct' them into pattern bindings. If you are dealing with data that can't be constructed trivially and unambiguously, like generics, traits, or private data, then you can't pattern match its internal structure.

(I keep saying 'internal structure', because you can match anything with a simple variable, like let x = ....)

The == operator calls user-defined (arbitrary) code, so it cannot be used to create patterns either. Nor can anything constructed through a function call. Only language builtin constructors can be used. So if you need to pattern match something more complex, you have to convert it into something constructable first.

3 Likes

Ah ok so basically you can only match on primitive types (or struct/enum/slices of primitive types) because it uses built in comparison logic rather than ==?

Shame.

If the overall "shape" of the patterns you need to match is small enough, this may be an acceptable alternative:

pub fn info2<'a, S>(path: &[S]) -> Option<String>
where
    S: AsRef<str>,
{
    match path {
        [a, b] => match [a.as_ref(), b.as_ref()] {
            ["a", "b"] => Some("It's a b!".to_string()),
            _ => None,
        },
        _ => None,
    }
}

The key phrase around the pattern matching approach is "structural equality", and you can read about it in this RFC. There's also some recent discussion in this open issue.

1 Like

While match directly can't do this, it is possible to write a macro to do this nicely:

list_match!(path, 
 { "a", {
    "b" => 42,
    "c" => 23,
    _ => -2,
  },
 "d" => 0,
 _ => -1,
})

which expands to something like

match list.get(0).map(|s| s.as_str()) {
  Some("a") => {
    match list.get(1).map(|s| s.as_str()) {
       Some("b") => 42,
       Some("c") => 23,
       _ => -2
    }
    Some("d") => 0,
    _ => -1
}
1 Like