Scala doppia definizione (2 metodi hanno lo stesso tipo di cancellazione)

Ho scritto questo in scala e non verrà compilato:

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

il compilatore 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 

So che JVM non ha supporto nativo per i generici quindi ho capito questo errore.

Potrei scrivere wrapper per List[String] e List[Int] ma sono pigro 🙂

Sono dubbioso, ma esiste un altro modo per esprimere List[String] non dello stesso tipo di List[Int] ?

Grazie.

Mi piace l’idea di Michael Krämer di usare impliciti, ma penso che possa essere applicata più direttamente:

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

Penso che questo sia abbastanza leggibile e diretto.

[Aggiornare]

C’è un altro modo semplice che sembra funzionare:

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

Per ogni versione è necessario un parametro di tipo aggiuntivo, quindi questo non è in scala, ma penso che per tre o quattro versioni va bene.

[Aggiornamento 2]

Per esattamente due metodi ho trovato un altro bel trucco:

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

Invece di inventare valori impliciti fittizi, puoi usare il DummyImplicit definito in Predef che sembra essere fatto esattamente per quello:

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

A causa delle meraviglie della cancellazione dei tipi, i parametri dei tipi dell’elenco dei metodi vengono cancellati durante la compilazione, riducendo così entrambi i metodi alla stessa firma, che è un errore del compilatore.

Per comprendere la soluzione di Michael Krämer , è necessario riconoscere che i tipi di parametri impliciti non sono importanti. Ciò che è importante è che i loro tipi siano distinti.

Il seguente codice funziona allo stesso modo:

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

A livello bytecode, entrambi i metodi foo diventano metodi a due argomenti poiché il bytecode JVM non sa nulla dei parametri impliciti o di più elenchi di parametri. Nel callsite, il compilatore Scala seleziona il metodo foo appropriato per chiamare (e quindi l’object fittizio appropriato da passare) esaminando il tipo di elenco che viene passato (che non viene cancellato fino a un momento successivo).

Mentre è più prolisso, questo approccio allevia il chiamante dall’onere di fornire gli argomenti impliciti. In effetti, funziona anche se gli oggetti dummyN sono privati ​​della class TestDoubleDef .

Come dice già Viktor Klang, il tipo generico verrà cancellato dal compilatore. Fortunatamente, c’è una soluzione:

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

Grazie per Michid per il suggerimento!

Se combino la risposta di Daniel e la risposta di Sandor Murakozi qui ottengo:

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

Ottengo una variante tipicamente (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)) :21: error: Type Long not supported only Int and String accepted a.foo(List(1L,2L,3L)) ^ scala> a.foo("test") :9: error: type mismatch; found : java.lang.String("test") required: List[?] a.foo("test") ^ 

La logica può anche essere inclusa nella class del tipo in quanto tale (grazie a jsuereth ): @ annotation.implicitNotFound (msg = “Foo non supporta $ {T} solo Int e String accettato”) tratto chiuso Foo [T] {def apply (lista: lista [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) 

Che dà:

 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) :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)) :32: error: Foo does not support Double only Int and String accepted foo(List(1.0)) ^ 

Nota che dobbiamo scrivere implicitly[Foo[A]].apply(x) poiché il compilatore pensa che implicitly[Foo[A]](x) significa che chiamiamo implicitly con i parametri.

C’è (almeno uno) un altro modo, anche se non è troppo bello e non è veramente sicuro:

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

Il paramenter manifest manifest può essere usato per “reificare” il tipo cancellato e quindi hackerare la cancellazione. Puoi imparare un po ‘di più su di esso in molti post del blog, ad esempio questo .

Quello che succede è che il parametro manifest può restituirti ciò che era prima della cancellazione. Quindi un semplice dispacciamento basato su T per le varie realizzazioni fa il resto.

Probabilmente c’è un modo più bello per fare la corrispondenza del modello, ma non l’ho ancora visto. Quello che le persone di solito fanno è l’abbinamento su m.toString, ma penso che mantenere le classi sia un po ‘più pulito (anche se è un po’ più prolisso). Sfortunatamente la documentazione di Manifest non è troppo dettagliata, forse ha anche qualcosa che potrebbe semplificarlo.

Un grosso svantaggio è che non è veramente sicuro: foo sarà felice con qualsiasi T, se non puoi gestirlo avrai un problema. Immagino che potrebbe essere aggirato con alcuni vincoli su T, ma complicherebbe ulteriormente.

E ovviamente tutta questa roba non è troppo bella, non sono sicuro che valga la pena farlo, specialmente se sei pigro 😉

Anziché utilizzare i manifesti, è ansible utilizzare anche oggetti dispatcher implicitamente importati in modo simile. Ho parlato di questo prima che comparissero i manifest: http://michid.wordpress.com/code/implicit-double-dispatch-revisited/

Questo ha il vantaggio della sicurezza del tipo: il metodo sovraccaricato sarà chiamabile solo per i tipi che hanno importatori inviati nello scope corrente.

Ho provato a migliorare le risposte di Aaron Novstrup e Leo per rendere un set di oggetti di prova standard più semplici e più concisi.

 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 } 

Ma ciò farà sì che il compilatore si lamenti che ci sono scelte ambigue per il valore implicito quando foo chiama un altro metodo che richiede un parametro implicito dello stesso tipo.

Quindi offro solo il seguente che è più conciso in alcuni casi. E questo miglioramento funziona con classi di valore (quelle che 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 } 

Se il nome del tipo contenente è piuttosto lungo, dichiara un trait interiore per renderlo più conciso.

 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 } 

Tuttavia, le classi di valore non consentono tratti interni, classi o oggetti. Pertanto, si noti anche che le risposte di Aaron Novstrup e Leo non funzionano con classi di valore.

Bel trucco che ho trovato da http://scala-programming-language.1934581.n4.nabble.com/disambiguation-of-double-definition-resulting-from-generic-type-erasure-td2327664.html di Aaron Novstrup

Battere questo cavallo morto ancora un po ‘…

Mi è venuto in mente che un hack più pulito è quello di utilizzare un tipo fittizio unico per ogni metodo con tipi cancellati nella sua 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 } 

[…]

Non ho provato questo, ma perché non dovrebbe funzionare un limite superiore?

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

La traduzione di cancellazione cambia da foo (elenca [Any] s) due volte, a foo (List [String] s) e foo (List [Int] i):

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

Penso di aver letto che nella versione 2.8, i limiti superiori ora sono codificati in quel modo, invece di sempre Qualsiasi.

Per sovraccaricare i tipi covarianti, usa un limite invariante (esiste una syntax simile in Scala? ... ah, penso che non ci sia, ma prendi il seguente addendum concettuale alla soluzione principale di cui sopra):

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

quindi presumo che il cast implicito sia eliminato nella versione cancellata del codice.


AGGIORNAMENTO: il problema è che JVM cancella più informazioni di tipo sulle firme dei metodi di quelle "necessarie". Ho fornito un link. Cancella le variabili di tipo dai costruttori di tipi, anche il limite concreto di quelle variabili di tipo. C'è una distinzione concettuale, perché non esiste alcun vantaggio concettuale non reificato per cancellare il tipo di collegamento della funzione, come è noto in fase di compilazione e non varia con nessuna istanza del generico, ed è necessario che i chiamanti non chiamino la funzione con tipi che non sono conformi al tipo associato, quindi come può la JVM imporre il tipo associato se è cancellato? Bene, un link dice che il tipo vincolato viene mantenuto nei metadati a cui i compilatori devono accedere. E questo spiega perché l'uso dei limiti di tipo non abilita il sovraccarico. Significa anche che JVM è un buco di sicurezza molto aperto poiché i metodi con limiti di tipo possono essere chiamati senza limiti di tipo (yikes!), Quindi scusami se supponiamo che i progettisti JVM non facciano una cosa così insicura.

All'epoca in cui scrissi ciò, non capivo che lo StackOverflow fosse un sistema di valutazione delle persone in base alla qualità delle risposte, come una certa concorrenza sulla reputazione. Pensavo fosse un posto dove condividere informazioni. Nel momento in cui scrissi questo, stavo confrontando reificato e non-reificato da un livello concettuale (confrontando molte lingue diverse), e quindi nella mia mente non aveva alcun senso cancellare il tipo di bounding.