Как использовать сопоставление с образцом с параметризованными признаками?

У меня проблемы с чертами Scala и стиранием типов. У меня есть эта черта:

trait Meta[T] {
  def ~=(e: T): Boolean
}

Теперь я хочу использовать сопоставление с образцом для проверки этого случая:

(m,i) match {
case (x:Meta[T], y: T) if x ~= y => println ("right")
case _ => println ("wrong")}

T из x: Meta[T] должен быть типом y или y должен быть подтипом T. Если типы не совпадают, я получаю ClassCastException. Но x ~= y не следует выполнять, если типы неверны. Есть ли обходной путь или мне нужно поймать исключение и обработать его таким образом?

Я сделал рабочий пример как можно короче:

trait Meta[T] {
  type t = T
  def ~=(e: T): Boolean
}

sealed abstract class A 
case class Ide(s: String) extends A
case class MIde(s: String) extends A with Meta[A] {
  def ~=(e: A) = e match {
    case e: Ide => true
    case e: MIde => false
  }
}
sealed abstract class B 
case class Foo(s: String) extends B

object Test {

  def m = MIde("x")
  def i = Ide("i")
  def f = Foo("f")

  def main[T](args: Array[String]) {
    (m, i) match {
      case (x: Meta[T], y: T) if x ~= y => println("right")
      case _ => println("wrong")
    }
    // -> right
    (m, f) match {
      case (x: Meta[T], y: T) if x ~= y => println("right")
      case _ => println("wrong")
    }
    // -> Exception in thread "main" java.lang.ClassCastException: 
    // Examples.Foo cannot be cast to Examples.A

  }
}

person schlicht    schedule 04.10.2012    source источник


Ответы (1)


ОБНОВЛЕНИЕ: добавлена ​​альтернатива в конце.

Вы сталкиваетесь с ограничениями сопоставления шаблонов, когда речь идет об общих типах, из-за стирания типов.

Однако не все потеряно. Мы можем положиться на ClassManifests для реализации универсального метода для преобразования ваших классов в целевой тип T (и другой аналогичный для преобразования в Meta[T]):

trait Meta[T] { this: T =>
  type t = T
  def metaManifest: ClassManifest[T]
  def ~=(e: T): Boolean
}

abstract sealed class Base {
  def as[T:ClassManifest]: Option[T] = {
    if ( classManifest[T].erasure.isAssignableFrom( this.getClass ) ) Some( this.asInstanceOf[T] )
    else None
  }
  def asMeta[T:ClassManifest]: Option[T with Meta[T]] = {
    this match {
      case meta: Meta[_] if classManifest[T] <:< meta.metaManifest => as[T].asInstanceOf[Option[T with Meta[T]]]
      case _ => None
    }
  }
}
abstract sealed class A extends Base
case class Ide(s: String) extends A
case class MIde(s: String) extends A with Meta[A] {
  val metaManifest = classManifest[A]
  def ~=(e: A) = e match {
    case e: Ide => true
    case e: MIde => false
  }
}
sealed abstract class B extends Base
case class Foo(s: String) extends B

Давайте проверим это в REPL:

scala> m.as[A]
res17: Option[A] = Some(MIde(x))
scala> m.asMeta[A]
res18: Option[A with Meta[A]] = Some(MIde(x))
scala> i.as[A]
res19: Option[A] = Some(Ide(i))
scala> i.asMeta[A]
res20: Option[A with Meta[A]] = None
scala> f.as[A]
res21: Option[A] = None
scala> f.asMeta[A]
res22: Option[A with Meta[A]] = None

Звучит неплохо. Теперь мы можем переписать наше сопоставление с образцом следующим образом:

(m, i) match {
  case (x: Meta[T], y: T) if x ~= y => println("right")
  case _ => println("wrong")
}

к этому:

(m.asMeta[T], i.as[T]) match {
  case (Some(x), Some(y)) if x ~= y => println("right")
  case _ => println("wrong")
}

Итак, ваш пример теперь будет выглядеть так:

object Test {

  val m = MIde("x")
  val i = Ide("i")
  val f = Foo("f")

  def test[T:ClassManifest]() {
    (m.asMeta[T], i.as[T]) match {
      case (Some(x), Some(y)) if x ~= y => println("right")
      case _ => println("wrong")
    }
    // -> right
    (m.asMeta[T], f.as[T]) match {
      case (Some(x), Some(y)) if x ~= y => println("right")
      case _ => println("wrong")
    }
  }
}

ОБНОВЛЕНИЕ: если установка явно metaManifest каждый раз, когда вы смешиваете Meta, невозможна, вы можете позволить scala автоматически выводить его, неявно передавая его в конструкторе Metas. Это означает, что Meta теперь должен быть классом, и, как следствие, A и B (и все подобные типы, которые должны отображаться как параметр типа Meta) теперь должны быть трейтами, так как вы не можете смешивать 2 класса. Таким образом, вы фактически меняете ограничение на другое. Выберите свой любимый. Вот так:

abstract sealed class Meta[T]( implicit val metaManifest: ClassManifest[T] ) { this: T =>
  type t = T
  def ~=(e: T): Boolean
}

trait Base {
  def as[T:ClassManifest]: Option[T] = {
    if ( classManifest[T].erasure.isAssignableFrom( this.getClass ) ) Some( this.asInstanceOf[T] )
    else None
  }
  def asMeta[T:ClassManifest]: Option[T with Meta[T]] = {
    this match {
      case meta: Meta[_] if classManifest[T] != ClassManifest.Nothing && classManifest[T] <:< meta.metaManifest => as[T].asInstanceOf[Option[T with Meta[T]]]
      case _ => None
    }
  }
}

trait A extends Base
case class Ide(s: String) extends A
case class MIde(s: String) extends Meta[A] with A {
  def ~=(e: A) = e match {
    case e: Ide => true
    case e: MIde => false
  }
}
trait B extends Base
case class Foo(s: String) extends B

object Test {
  val m = MIde("x")
  val i = Ide("i")
  val f = Foo("f")

  def test[T:ClassManifest]() {
    (m.asMeta[T], i.as[T]) match {
      case (Some(x), Some(y)) if x ~= y => println("right")
      case _ => println("wrong")
    }
    (m.asMeta[T], f.as[T]) match {
      case (Some(x), Some(y)) if x ~= y => println("right")
      case _ => println("wrong")
    }
  }
}

Наконец, если ни одно из решений вам не подходит, вы можете попробовать другое: вместо того, чтобы смешивать Meta[T] с T, просто оберните его. Тогда Meta[T] будет просто оболочкой для T, и вы даже можете добавить неявное преобразование из Meta[T] в его обернутое значение, чтобы экземпляр Meta[T] можно было эффективно использовать как экземпляр T почти прозрачно.

person Régis Jean-Gilles    schedule 04.10.2012
comment
Спасибо за ваш ответ, но, к сожалению, я все еще получаю ClassCaseException даже с вашим решением. - person schlicht; 05.10.2012
comment
Приведенный выше код работает для меня. Можете ли вы уточнить, какой именно код не работает? Что вы делаете иначе, чем в моем фрагменте кода? - person Régis Jean-Gilles; 05.10.2012
comment
Я вставил весь фрагмент кода в интерпретатор Scala, затем я запускаю Test.test(): Test.test() справа java.lang.ClassCastException: Foo не может быть приведен к A - person schlicht; 05.10.2012
comment
Ах хорошо. Попробуйте Test.test[A] и Test.test[String]. Если вы не укажете какой-либо параметр типа, scala выведет Nothing, что здесь не очень полезно. Честно говоря, мой фрагмент кода ошибочен, поскольку он не будет работать правильно для T = Nothing (или даже T = любой супертип Any и T = AnyRef). Здесь я совершил роковую ошибку, из-за которой это совершенно небезопасно для типов. Поэтому я обновил свой пост, чтобы исправить это. Однако обратите внимание, что теперь мы должны предоставлять classManifest для параметра типа Meta явно (каждый раз, когда мы смешиваем его), что чрезвычайно громоздко. Изменение дизайна вокруг может быть хорошей идеей. - person Régis Jean-Gilles; 05.10.2012
comment
Предоставление classManifest каждый раз, когда Meta смешивается, нецелесообразно, потому что этот код будет частью библиотеки и внутренних компонентов, поскольку это не должно быть раскрыто. Как я могу получить тип для вызова Test.test[TYPE] во время выполнения? Есть ли способ через classManifest? Я не очень понимаю трюк с манифестом в целом, но я попытаюсь его понять! Спасибо! - person schlicht; 05.10.2012
comment
Спасибо за помощь! Сейчас я использую отражение, чтобы проверить, соответствует ли тип параметра ~= заданному аргументу во время выполнения. Когда разработка продвинется, я еще раз рассмотрю ваши решения и решу, какое из них наиболее подходит. - person schlicht; 08.10.2012