Definición doble de Scala(2 métodos tienen el mismo tipo de borrado)
compilation method-overloading (11)
Buen truco que he encontrado en http://scala-programming-language.1934581.n4.nabble.com/disambiguation-of-double-definition-resulting-from-generic-type-erasure-td2327664.html por Aaron Novstrup
Venciendo a este caballo muerto un poco más ...
Se me ocurrió que un truco más limpio es usar un tipo ficticio único para cada método con tipos borrados en su firma:
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
}
[...]
Escribí esto en scala y no compilará:
class TestDoubleDef{
def foo(p:List[String]) = {}
def foo(p:List[Int]) = {}
}
el compilador notifica:
[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
Sé que JVM no tiene soporte nativo para los genéricos, así que entiendo este error.
Podría escribir wrappers para List[String]
y List[Int]
pero soy flojo :)
No estoy seguro, pero ¿hay otra forma de expresar List[String]
no es del mismo tipo que List[Int]
?
Gracias.
Como Viktor Klang ya dice, el tipo genérico será borrado por el compilador. Afortunadamente, hay una solución:
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)
}
Gracias por Michid por el consejo!
Debido a las maravillas del borrado de tipos, los parámetros de tipo de la lista de sus métodos se borran durante la compilación, reduciendo así ambos métodos a la misma firma, que es un error del compilador.
En lugar de inventar valores implícitos ficticios, puede usar el DummyImplicit
definido en Predef
que parece estar hecho exactamente para eso:
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) = ()
}
En lugar de usar manifiestos, también podría usar despachadores objetos implícitamente importados de manera similar. Publiqué sobre esto antes de que aparecieran manifiestos: http://michid.wordpress.com/code/implicit-double-dispatch-revisited/
Esto tiene la ventaja de seguridad tipo: el método sobrecargado solo se podrá llamar para los tipos que tengan importadores importados en el alcance actual.
Hay (al menos uno) de otra manera, incluso si no es demasiado bueno y no es realmente seguro.
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))
}
}
El parámetro de manifiesto implícito se puede utilizar para "reificar" el tipo borrado y así piratear el borrado. Puede aprender un poco más sobre esto en muchas publicaciones de blog, por ejemplo, esta .
Lo que sucede es que el param manifiesto puede devolverte lo que T era antes del borrado. Entonces, un simple despacho basado en T a la implementación real de varios hace el resto.
Probablemente haya una forma más agradable de hacer la coincidencia de patrones, pero aún no la he visto. Lo que la gente suele hacer es combinar en m.toString, pero creo que mantener las clases es un poco más limpio (incluso si es un poco más detallado). Lamentablemente, la documentación de Manifest no es demasiado detallada, quizás también tenga algo que podría simplificarla.
Una gran desventaja de esto es que no es realmente seguro: Foo estará contento con cualquier T, si no puedes manejarlo, tendrás un problema. Creo que podría solucionarse con algunas limitaciones en T, pero lo complicaría aún más.
Y, por supuesto, todo esto tampoco es demasiado agradable, no estoy seguro si vale la pena hacerlo, especialmente si eres perezoso ;-)
Me gusta la idea de Michael Krämer de utilizar implícitos, pero creo que se puede aplicar más directamente:
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)}
Creo que esto es bastante legible y directo.
[Actualizar]
Hay otra manera fácil que parece funcionar:
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") }
Para cada versión necesita un parámetro de tipo adicional, por lo que no se escala, pero creo que para tres o cuatro versiones está bien.
[Actualización 2]
Para exactamente dos métodos encontré otro buen truco:
def foo(list: => List[Int]) = { println("Int-List " + list)}
def foo(list: List[String]) = { println("String-List " + list)}
No probé esto, pero ¿por qué no funcionaría un límite superior?
def foo[T <: String](s: List[T]) { println("Strings: " + s) }
def foo[T <: Int](i: List[T]) { println("Ints: " + i) }
¿La traducción de borrado cambia de foo (Lista [Cualquiera] s) dos veces, a foo (Lista [String] s) y foo (Lista [Int] i):
http://www.angelikalanger.com/GenericsFAQ/FAQSections/TechnicalDetails.html#FAQ108
Creo que leí eso en la versión 2.8, los límites superiores ahora están codificados de esa manera, en lugar de siempre un Any.
Para sobrecargar en tipos covariantes, utilice un enlace invariante (¿existe tal sintaxis en Scala? ... ah, creo que no, pero tome lo siguiente como un apéndice conceptual a la solución principal anterior):
def foo[T : String](s: List[T]) { println("Strings: " + s) }
def foo[T : String2](s: List[T]) { println("String2s: " + s) }
entonces supongo que la conversión implícita se elimina en la versión borrada del código.
ACTUALIZACIÓN: El problema es que JVM borra más información de tipo en las firmas de métodos de lo que es "necesario". Proporcioné un enlace. Borra variables de tipo de constructores de tipo, incluso el límite concreto de esas variables de tipo. Existe una distinción conceptual, ya que no existe una ventaja conceptual no reificada para borrar el tipo enlazado de la función, ya que se conoce en tiempo de compilación y no varía con ninguna instancia del genérico, y es necesario que las personas que llaman no llamen la función con tipos que no se ajustan al tipo enlazado, entonces ¿cómo puede la JVM imponer el tipo enlazado si se borra? Bueno, un enlace dice que el límite de tipo se retiene en los metadatos a los que los compiladores deben acceder. Y esto explica por qué el uso de límites de tipo no permite la sobrecarga. También significa que JVM es un agujero de seguridad muy abierto ya que se pueden llamar a métodos de tipo limitado sin límites de tipo (¡yikes!), Así que discúlpeme por suponer que los diseñadores de JVM no harían algo tan inseguro.
En el momento en que escribí esto, no entendía que era un sistema de clasificación de personas por calidad de respuestas, como alguna competencia por reputación. Pensé que era un lugar para compartir información. En el momento en que escribí esto, estaba comparando reificado y no reificado desde un nivel conceptual (comparando muchos idiomas diferentes), y en mi opinión no tenía ningún sentido borrar el tipo vinculado.
Para entender la solución de Michael Krämer , es necesario reconocer que los tipos de los parámetros implícitos no son importantes. Lo importante es que sus tipos son distintos.
El siguiente código funciona de la misma manera:
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)
}
En el nivel de bytecode, ambos métodos foo
convierten en métodos de dos argumentos, ya que el bytecode de JVM no conoce nada de parámetros implícitos o listas de parámetros múltiples. En el callsite, el compilador de Scala selecciona el método foo
apropiado para llamar (y por lo tanto el objeto ficticio apropiado para pasar) mirando el tipo de la lista que se pasa (que no se borra hasta más adelante).
Si bien es más detallado, este enfoque alivia al llamador de la carga de proporcionar los argumentos implícitos. De hecho, incluso funciona si los objetos dummyN son privados para la clase TestDoubleDef
.
Si combino la response Daniel y la response Sandor Murakozi aquí obtengo:
@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")
}
}
}
Obtengo una variante typesafe (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")
^
La lógica también se puede incluir en la clase de tipo como tal (gracias a jsuereth ): @ annotation.implicitNotFound (msg = "Foo no admite $ {T} solo Int y String aceptados") rasgo sellado Foo [T] {def apply (lista: Lista [T]): Unidad}
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)
Lo que da:
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))
^
Tenga en cuenta que tenemos que escribir implicitly[Foo[A]].apply(x)
ya que el compilador cree que implicitly[Foo[A]](x)
significa que llamamos implicitly
con parámetros.
Traté de mejorar las respuestas de Aaron Novstrup y Leo para hacer que un conjunto de objetos de evidencia estándar sea más preciso y más preciso.
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
}
Pero eso hará que el compilador se queje de que hay opciones ambiguas para el valor implícito cuando foo
llama a otro método que requiere un parámetro implícito del mismo tipo.
Por lo tanto, ofrezco solo lo siguiente, que en algunos casos es más escueto. Y esta mejora funciona con clases de valor (las que 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
}
Si el nombre del tipo que contiene es bastante largo, declare un trait
interno para hacerlo más escueto.
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
}
Sin embargo, las clases de valores no permiten rasgos internos, clases ni objetos. Por lo tanto, también tenga en cuenta que las respuestas de Aaron Novstrup y Leo no funcionan con las clases de valores.