Search code examples
scalapattern-matching

Scala nested match pattern matching


I'm writing a sort of rules engine that traverses a tree and establishes a context for each sub-tree and then uses match conditions to validate the tree and infer information about the nodes.

I had hoped that I could use nested match conditions (on case class constructors) and match on outer and inner properties by using the same constructor property name like this:

val context = Foo(42, "Hello, World")
val items = List(
  Foo(42, "Hello, World"),
  Foo(42, "Bar"),
  Foo(24, "Hello, World"),
  Foo(2, "Goodbye")
)

def matchFoo(context: Foo, item: Foo): String = {
  context match {
    case Foo(num, text) =>
      item match {
        case Foo(num, text)           => "Exact match"
        case Foo(otherNum, text)      => "Text match"
        case Foo(num, otherText)      => "Number match"
        case Foo(otherNum, otherText) => "No match"
      }
  }

}

items.map(i => matchFoo(context, i))

But I just get 4 "Exact match" results. I'm guessing that the inner match of case Foo(num, text) hides the outer num, text variables and so matches any Foo item.

I can get around the issue by using explicit guard conditions but this is what I was trying to avoid:

val context = Foo(42, "Hello, World")
val items = List(
  Foo(42, "Hello, World"),
  Foo(42, "Bar"),
  Foo(24, "Hello, World"),
  Foo(2, "Goodbye")
)

def matchFoo(context: Foo, item: Foo): String = {
  context match {
    case Foo(num, text) =>
      item match {
        case Foo(otherNum, otherText) if otherNum == num && otherText == text => "Exact match"
        case Foo(otherNum, _) if otherNum == num => "Number match"
        case Foo(_, otherText) if otherText == text => "Text match"
        case _ => "No match"
      }
  }

}

items.map(i => matchFoo(context, i))

Is there anyway to achieve this or is guard conditions the way to go?


Solution

  • You are correct that num and text in the inner pattern create new variable bindings that hide the outer ones, instead of using the values of the outer variables as values to match. You can quote the names with backticks (`) to get the behaviour you want:

    case class Foo(num: Int, text: String)
    val context = Foo(42, "Hello, World")
    val items = List(
      Foo(42, "Hello, World"),
      Foo(42, "Bar"),
      Foo(24, "Hello, World"),
      Foo(2, "Goodbye")
    )
    
    def matchFoo(context: Foo, item: Foo): String = {
      context match {
        case Foo(num, text) =>
          item match {
            case Foo(`num`, `text`)       => "Exact match"
            case Foo(otherNum, `text`)    => "Text match"
            case Foo(`num`, otherText)    => "Number match"
            case Foo(otherNum, otherText) => "No match"
          }
      }
    
    }
    
    items.map(i => matchFoo(context, i))
    

    Output:

    val res0: List[String] = List(Exact match, Number match, Text match, No match)
    

    This syntax is described in the Scala Language Specification chapter on Pattern Matching, in the section titled "Stable Identifier Patterns":

    A stable identifier pattern is a stable identifier r. The type of r must conform to the expected type of the pattern. The pattern matches any value v such that r == v (see here).

    To resolve the syntactic overlap with a variable pattern, a stable identifier pattern may not be a simple name starting with a lower-case letter. However, it is possible to enclose such a variable name in backquotes; then it is treated as a stable identifier pattern.

    Example Consider the following class definition:

    class C { c =>
      val x = 42
      val y = 27
      val Z = 8
      def f(x: Int) = x match {
        case c.x => 1  // matches 42
        case `y` => 2  // matches 27
        case Z   => 3  // matches 8
        case x   => 4  // matches any value
      }
    }
    

    Here, the first three patterns are stable identifier patterns, while the last one is a variable pattern.