Двойное определение Scala (2 метода имеют стирание одного типа)

Я написал это в Scala, и он не будет компилироваться:

class TestDoubleDef{
  def foo(p:List[String]) = {}
  def foo(p:List[Int]) = {}
}

компилятор уведомляет:

[error] double definition:
[error] method foo:(List[String])Unit and
[error] method foo:(List[Int])Unit at line 120
[error] have same type after erasure: (List)Unit

Я знаю, что у JVM нет встроенной поддержки обобщений, поэтому я понимаю эту ошибку.

Я мог бы написать обертки для List[String] а также List[Int] но я ленивый:)

Я сомневаюсь, но есть ли другой способ выразить List[String] не того же типа, чем List[Int]?

Благодарю.

11 ответов

Решение

Мне нравится идея Михаила Крамера использовать импликации, но я думаю, что она может быть применена более непосредственно:

case class IntList(list: List[Int])
case class StringList(list: List[String])

implicit def il(list: List[Int]) = IntList(list)
implicit def sl(list: List[String]) = StringList(list)

def foo(i: IntList) { println("Int: " + i.list)}
def foo(s: StringList) { println("String: " + s.list)}

Я думаю, что это вполне читабельно и просто.

[Обновить]

Есть еще один простой способ, который работает:

def foo(p: List[String]) { println("Strings") }
def foo[X: ClassManifest](p: List[Int]) { println("Ints") }
def foo[X: ClassManifest, Y: ClassManifest](p: List[Double]) { println("Doubles") }

Для каждой версии вам нужен дополнительный параметр типа, поэтому он не масштабируется, но я думаю, что для трех или четырех версий это нормально.

[Обновление 2]

Для ровно двух методов я нашел еще один приятный трюк:

def foo(list: => List[Int]) = { println("Int-List " + list)}
def foo(list: List[String]) = { println("String-List " + list)}

Вместо того, чтобы придумывать фиктивные неявные значения, вы можете использовать DummyImplicit определяется в Predef который, кажется, сделан именно для этого:

class TestMultipleDef {
  def foo(p:List[String]) = ()
  def foo(p:List[Int])(implicit d: DummyImplicit) = ()
  def foo(p:List[java.util.Date])(implicit d1: DummyImplicit, d2: DummyImplicit) = ()
}

Чтобы понять решение Михаэля Крамера, необходимо признать, что типы неявных параметров не важны. Важно то, что их типы различны.

Следующий код работает аналогично:

class TestDoubleDef {
   object dummy1 { implicit val dummy: dummy1.type = this }
   object dummy2 { implicit val dummy: dummy2.type = this }

   def foo(p:List[String])(implicit d: dummy1.type) = {}
   def foo(p:List[Int])(implicit d: dummy2.type) = {}
}

object App extends Application {
   val a = new TestDoubleDef()
   a.foo(1::2::Nil)
   a.foo("a"::"b"::Nil)
}

На уровне байт-кода оба foo методы становятся методами с двумя аргументами, поскольку байт-код JVM ничего не знает о неявных параметрах или списках с несколькими параметрами. На месте вызова компилятор Scala выбирает соответствующий foo метод для вызова (и, следовательно, соответствующий фиктивный объект для передачи), посмотрев на тип передаваемого списка (который не удаляется до позднего времени).

Хотя этот подход более многословен, этот подход освобождает вызывающего абонента от бремени предоставления неявных аргументов. На самом деле, это даже работает, если объекты dummyN являются частными для TestDoubleDef учебный класс.

Из-за чудес стирания типов параметры типа List ваших методов стираются во время компиляции, таким образом сводя оба метода к одной и той же сигнатуре, что является ошибкой компилятора.

Как уже сказал Виктор Кланг, универсальный тип будет удален компилятором. К счастью, есть обходной путь:

class TestDoubleDef{
  def foo(p:List[String])(implicit ignore: String) = {}
  def foo(p:List[Int])(implicit ignore: Int) = {}
}

object App extends Application {
  implicit val x = 0
  implicit val y = ""

  val a = new A()
  a.foo(1::2::Nil)
  a.foo("a"::"b"::Nil)
}

Спасибо за Михид за совет!

Если я Sandor Murakozi ответ Daniel C. Sobral и ответ Sandor Murakozi здесь, я получу:

@annotation.implicitNotFound(msg = "Type ${T} not supported only Int and String accepted")   
sealed abstract class Acceptable[T]; object Acceptable {
        implicit object IntOk extends Acceptable[Int]
        implicit object StringOk extends Acceptable[String]
}

class TestDoubleDef {
   def foo[A : Acceptable : Manifest](p:List[A]) =  {
        val m = manifest[A]
        if (m equals manifest[String]) {
            println("String")
        } else if (m equals manifest[Int]) {
            println("Int")
        } 
   }
}

Я получаю типобезопасный (ish) вариант

scala> val a = new TestDoubleDef
a: TestDoubleDef = TestDoubleDef@f3cc05f

scala> a.foo(List(1,2,3))
Int

scala> a.foo(List("test","testa"))
String

scala> a.foo(List(1L,2L,3L))
<console>:21: error: Type Long not supported only Int and String accepted
   a.foo(List(1L,2L,3L))
        ^             

scala> a.foo("test")
<console>:9: error: type mismatch;
 found   : java.lang.String("test")
 required: List[?]
       a.foo("test")
             ^

Логика также может быть включена в класс типов как таковой (благодаря jsuereth): @ annotation.implicitNotFound (msg = "Foo не поддерживает ${T} только Int и String принимаются") sealed trait Foo[T] { def apply(список: список [T]): блок}

object Foo {
   implicit def stringImpl = new Foo[String] {
      def apply(list : List[String]) = println("String")
   }
   implicit def intImpl = new Foo[Int] {
      def apply(list : List[Int]) =  println("Int")
   }
} 

def foo[A : Foo](x : List[A]) = implicitly[Foo[A]].apply(x)

Который дает:

scala> @annotation.implicitNotFound(msg = "Foo does not support ${T} only Int and String accepted") 
     | sealed trait Foo[T] { def apply(list : List[T]) : Unit }; object Foo {
     |         implicit def stringImpl = new Foo[String] {
     |           def apply(list : List[String]) = println("String")
     |         }
     |         implicit def intImpl = new Foo[Int] {
     |           def apply(list : List[Int]) =  println("Int")
     |         }
     |       } ; def foo[A : Foo](x : List[A]) = implicitly[Foo[A]].apply(x)
defined trait Foo
defined module Foo
foo: [A](x: List[A])(implicit evidence$1: Foo[A])Unit

scala> foo(1)
<console>:8: error: type mismatch;
 found   : Int(1)
 required: List[?]
       foo(1)
           ^    
scala> foo(List(1,2,3))
Int
scala> foo(List("a","b","c"))
String
scala> foo(List(1.0))
<console>:32: error: Foo does not support Double only Int and String accepted
foo(List(1.0))
        ^

Обратите внимание, что мы должны написать implicitly[Foo[A]].apply(x) так как компилятор считает, что implicitly[Foo[A]](x) означает, что мы называем implicitly с параметрами.

Есть (по крайней мере, один) другой способ, даже если он не слишком приятен и не совсем безопасен:

import scala.reflect.Manifest

object Reified {

  def foo[T](p:List[T])(implicit m: Manifest[T]) = {

    def stringList(l: List[String]) {
      println("Strings")
    }
    def intList(l: List[Int]) {
      println("Ints")
    }

    val StringClass = classOf[String]
    val IntClass = classOf[Int]

    m.erasure match {
      case StringClass => stringList(p.asInstanceOf[List[String]])
      case IntClass => intList(p.asInstanceOf[List[Int]])
      case _ => error("???")
    }
  }


  def main(args: Array[String]) {
      foo(List("String"))
      foo(List(1, 2, 3))
    }
}

Параметр неявного манифеста может быть использован для "восстановления" стертого типа и, таким образом, взлома при удалении. Вы можете узнать немного больше об этом во многих блогах, например, в этом.

Что происходит, так это то, что проявленный параметр может вернуть вам то, что было до удаления. Затем простая отправка на основе T для различных реальных реализаций сделает все остальное.

Возможно, есть более хороший способ сопоставления с образцом, но я еще не видел его. То, что обычно делают люди, это сопоставление с m.toString, но я думаю, что ведение классов немного чище (даже если оно более многословно). К сожалению, документация Манифеста не слишком детализирована, возможно, в ней также есть что-то, что может упростить ее.

Большой недостаток этого метода в том, что он не совсем безопасен для типов: foo будет доволен любым T, если вы не справитесь с этим, у вас будут проблемы. Я думаю, что это можно обойти с некоторыми ограничениями на T, но это еще больше усложнит это.

И, конечно, все это тоже не слишком приятно, я не уверен, стоит ли это делать, особенно если вы ленивы;-)

Вместо использования манифестов вы также можете использовать диспетчерские объекты, неявно импортированные аналогичным образом. Я писал об этом до того, как появились манифесты: http://michid.wordpress.com/code/implicit-double-dispatch-revisited/

Это имеет преимущество безопасности типов: перегруженный метод будет вызываться только для типов, у которых диспетчеры импортированы в текущую область.

Я попытался улучшить ответы Аарона Новструпа и Лео, чтобы сделать один набор стандартных объектов доказательств импортируемым и более кратким.

final object ErasureEvidence {
    class E1 private[ErasureEvidence]()
    class E2 private[ErasureEvidence]()
    implicit final val e1 = new E1
    implicit final val e2 = new E2
}
import ErasureEvidence._

class Baz {
    def foo(xs: String*)(implicit e:E1) = 1
    def foo(xs: Int*)(implicit e:E2) = 2
}

Но это заставит компилятор жаловаться на то, что существуют неясные варианты неявного значения, когда foo вызывает другой метод, который требует неявного параметра того же типа.

Таким образом, я предлагаю только следующее, которое в некоторых случаях является более кратким. И это улучшение работает с классами значения (те, которые extend AnyVal).

final object ErasureEvidence {
   class E1[T] private[ErasureEvidence]()
   class E2[T] private[ErasureEvidence]()
   implicit def e1[T] = new E1[T]
   implicit def e2[T] = new E2[T]
}
import ErasureEvidence._

class Baz {
    def foo(xs: String*)(implicit e:E1[Baz]) = 1
    def foo(xs: Int*)(implicit e:E2[Baz]) = 2
}

Если имя содержащего типа довольно длинное, объявите внутренний trait чтобы сделать это более кратким.

class Supercalifragilisticexpialidocious[A,B,C,D,E,F,G,H,I,J,K,L,M] {
    private trait E
    def foo(xs: String*)(implicit e:E1[E]) = 1
    def foo(xs: Int*)(implicit e:E2[E]) = 2
}

Однако классы значений не допускают внутренних черт, классов или объектов. Также обратите внимание, что ответы Аарона Новструпа и Лео не работают с классами значений.

Хороший трюк, который я нашел из http://scala-programming-language.1934581.n4.nabble.com/disambiguation-of-double-definition-resulting-from-generic-type-erasure-td2327664.html Аарона Новструпа

Побить эту мертвую лошадь еще немного...

Мне пришло в голову, что более чистый взлом состоит в том, чтобы использовать уникальный фиктивный тип для каждого метода со стертыми типами в его сигнатуре:

object Baz {
    private object dummy1 { implicit val dummy: dummy1.type = this }
    private object dummy2 { implicit val dummy: dummy2.type = this } 

    def foo(xs: String*)(implicit e: dummy1.type) = 1
    def foo(xs: Int*)(implicit e: dummy2.type) = 2
} 

[...]

Я не проверял это, но почему не работает верхняя граница?

def foo[T <: String](s: List[T]) { println("Strings: " + s) }
def foo[T <: Int](i: List[T]) { println("Ints: " + i) }

Изменяется ли перевод стирания с foo( List[Any] s) дважды на foo( List[String] s) и foo (List [Int] i):

http://www.angelikalanger.com/GenericsFAQ/FAQSections/TechnicalDetails.html

Я думаю, что я читал, что в версии 2.8 верхние границы теперь кодируются таким образом, а не всегда Any.

Чтобы перегрузить ковариантные типы, используйте инвариантную границу (есть ли такой синтаксис в Scala?... а я думаю, что нет, но примите следующее как концептуальное дополнение к основному решению выше):

def foo[T : String](s: List[T]) { println("Strings: " + s) }
def foo[T : String2](s: List[T]) { println("String2s: " + s) }

затем я предполагаю, что неявное приведение исключено в стертой версии кода.


ОБНОВЛЕНИЕ: Проблема в том, что JVM удаляет больше информации о типе сигнатур методов, чем это "необходимо". Я предоставил ссылку. Он удаляет переменные типа из конструкторов типов, даже конкретную границу этих переменных типов. Существует концептуальное различие, потому что нет никакого концептуального нереализованного преимущества для стирания границы типа функции, как это известно во время компиляции и не зависит от какого-либо экземпляра универсального, и для вызывающих абонентов необходимо не вызывать функция с типами, которые не соответствуют ограничению типа, так как же JVM может принудительно установить ограничение типа, если оно стерто? Хорошо, одна ссылка говорит, что граница типа сохраняется в метаданных, к которым должны обращаться компиляторы. И это объясняет, почему использование границ типов не допускает перегрузки. Это также означает, что JVM - это широко открытая дыра в безопасности, поскольку ограниченные по типу методы могут вызываться без ограничений по типу (yikes!), Поэтому извините за то, что разработчики JVM не сделали бы такой небезопасной вещи.

В то время, когда я писал это, я не понимал, что stackru - это система оценки людей по качеству ответов, как, например, конкуренция за репутацию. Я думал, что это место для обмена информацией. В то время, когда я писал это, я сравнивал reified и non-reified с концептуального уровня (сравнивая много разных языков), и поэтому, по моему мнению, не было никакого смысла стирать границу типа.

Другие вопросы по тегам