Coincidir con varias clases de casos en scala

99

Estoy haciendo una comparación con algunas clases de casos y me gustaría manejar dos de los casos de la misma manera. Algo como esto:

abstract class Foo
case class A extends Foo
case class B(s:String) extends Foo
case class C(s:String) extends Foo


def matcher(l: Foo): String = {
  l match {
    case A() => "A"
    case B(sb) | C(sc) => "B"
    case _ => "default"
  }
}

Pero cuando hago esto, aparece el error:

(fragment of test.scala):10: error: illegal variable in pattern alternative
    case B(sb) | C(sc) => "B"

Puedo hacerlo funcionar si elimino los parámetros de la definición de B y C, pero ¿cómo puedo hacer coincidir con los parámetros?

timdisney
fuente

Respuestas:

144

Parece que no le importan los valores de los parámetros de cadena y desea tratar a B y C de la misma manera, así que:

def matcher(l: Foo): String = {
  l match {
    case A() => "A"
    case B(_) | C(_) => "B"
    case _ => "default"
  }
}

Si debe, debe, debe extraer el parámetro y tratarlo en el mismo bloque de código, podría:

def matcher(l: Foo): String = {
  l match {
    case A() => "A"
    case bOrC @ (B(_) | C(_)) => {
      val s = bOrC.asInstanceOf[{def s: String}].s // ugly, ugly
      "B(" + s + ")"
    }
    case _ => "default"
  }
}

Aunque creo que sería mucho más limpio factorizar eso en un método:

def doB(s: String) = { "B(" + s + ")" }

def matcher(l: Foo): String = {
  l match {
    case A() => "A"
    case B(s) => doB(s)
    case C(s) => doB(s)
    case _ => "default"
  }
}
Mitch Blevins
fuente
Aunque mi ejemplo no lo muestra, necesito esos parámetros. Parece que tendré que usar un objeto. ¡Gracias!
Timdisney
4
¿Hay alguna razón por la que scala no permite "case A (aString) | case B (aString) => println (aString)"? Parece que siempre que el tipo de aString sea idéntico tanto para A como para B, debería estar permitido. Su último ejemplo parece que sería mejor no duplicar los casos B y C.
James Moore
36
Te iré uno más lejos. Creo que sería bueno que se case A(x) | B(x) => println(x)le permitiera que el tipo de xse establezca en el límite superior en el sistema de tipos de lo que sea que A (x) y B (x) produzcan.
Mitch Blevins
1
@MitchBlevins: puede votar por issues.scala-lang.org/browse/SUGGEST-25 (permitir enlace de variables en un patrón alternativo)
Erik Kaplun
1
Para aquellos que se preguntan qué diablos está haciendo el símbolo @ allí: scala-lang.org/files/archive/spec/2.11/08-pattern-matching.html
SilentDirge
9

Hay un par de formas en las que puedo ver para lograr lo que buscas, si tienes algo en común entre las clases de casos. La primera es hacer que las clases de casos extiendan un rasgo que declare lo común, la segunda es usar un tipo estructural que elimina la necesidad de extender sus clases de casos.

 object MuliCase {
   abstract class Foo
   case object A extends Foo

   trait SupportsS {val s: String}

   type Stype = Foo {val s: String}

   case class B(s:String) extends Foo
   case class C(s:String) extends Foo

   case class D(s:String) extends Foo with SupportsS
   case class E(s:String) extends Foo with SupportsS

   def matcher1(l: Foo): String = {
     l match {
       case A        => "A"
       case s: Stype => println(s.s); "B"
       case _        => "default"
     }
   }

   def matcher2(l: Foo): String = {
     l match {
       case A            => "A"
       case s: SupportsS => println(s.s); "B"
       case _            => "default"
     }
   }

   def main(args: Array[String]) {
     val a = A
     val b = B("B's s value")
     val c = C("C's s value")

     println(matcher1(a))
     println(matcher1(b))
     println(matcher1(c))

     val d = D("D's s value")
     val e = E("E's s value")

     println(matcher2(d))
     println(matcher2(e))
   }
 }

El método de tipo estructural genera una advertencia sobre el borrado que, en este momento, no estoy seguro de cómo eliminar.

Don Mackenzie
fuente
6

Bueno, realmente no tiene sentido, ¿verdad? B y C son mutuamente excluyentes, por lo que sb o sc se vinculan, pero no sabes cuál, por lo que necesitarías más lógica de selección para decidir cuál usar (dado que estaban vinculados a una Option [String], no una cuerda). Así que no se gana nada con esto:

  l match {
    case A() => "A"
    case B(sb) => "B(" + sb + ")"
    case C(sc) => "C(" + sc + ")"
    case _ => "default"
  }

O esto:

  l match {
    case A() => "A"
    case _: B => "B"
    case _: C => "C"
    case _ => "default"
  }
Randall Schulz
fuente
¿Qué pasa si no le importa si B o C coincidieron? Diga en el siguiente código: args match { case Array("-x", hostArg) => (hostArg, true); case Array(hostArg, "-x") => (hostArg, true) }Sin embargo, veo que ese no es el caso común y que crear un método local es una alternativa. Sin embargo, si la alternativa es conveniente, entonces tiene poco sentido tener alternativas de caso. En realidad, en algunos dialectos ML tiene una característica similar y aún puede vincular variables, siempre que (IIRC) cada variable esté vinculada con el mismo tipo en ambas alternativas.
Blaisorblade
Estás en lo correcto. Si solo le interesan los tipos y no los valores ni el tipo que se presentó, la coincidencia disyuntiva basada en tipos es significativa y está disponible.
Randall Schulz