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]?

Спасибо.

Ответы

Ответ 1

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

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)}

Ответ 2

Вместо того, чтобы изобретать фиктивные неявные значения, вы можете использовать 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) = ()
}

Ответ 3

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

Ответ 4

Чтобы понять решение Michael Krämer, необходимо признать, что типы неявных параметров несущественны. Важно то, что их типы различны.

Следующий код работает таким же образом:

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 ничего не знает о неявных параметрах или нескольких списках параметров. В callsite компилятор Scala выбирает соответствующий метод foo для вызова (и, следовательно, соответствующего объекта-заглушки), просматривая тип передаваемого списка (который не удаляется до конца).

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

Ответ 5

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

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)
}

Спасибо за Michid за подсказку!

Ответ 6

Если я объединить Daniel ответ и Сандор 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")
        } 
   }
}

Я получаю вариант typeafe (ish)

scala> val a = new TestDoubleDef
a: TestDoubleDef = [email protected]

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" )   запечатанный признак 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)

Что дает:

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 с параметрами.

Ответ 7

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

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))
    }
}

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

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

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

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

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

Ответ 8

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

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

Ответ 9

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

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
}

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

Ответ 10

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

Избиение этой мертвой лошади еще...

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

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
} 

[...]

Ответ 11

Я не тестировал это, но почему бы не работать над верхней границей?

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#FAQ108

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

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

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 не будут делать такую ​​небезопасную вещь.

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