Scala Double Definition (2 Methoden haben den gleichen Löschtyp)

68

Ich habe dies in Scala geschrieben und es wird nicht kompiliert:

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

Der Compiler benachrichtigt:

[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

Ich weiß, dass JVM keine native Unterstützung für Generika hat, daher verstehe ich diesen Fehler.

Ich könnte Wrapper schreiben für List[String]und List[Int]aber ich bin faul :)

Ich bin zweifelhaft, aber gibt es eine andere Art des Ausdrucks, List[String]die nicht vom selben Typ ist wie List[Int]?

Vielen Dank.

Jérôme
quelle
1
Siehe auch stackoverflow.com/questions/3422336/…
Aaron Novstrup
6
Weiß jemand, warum Scala nicht automatisch verschiedene gelöschte Namen erstellt hat? Wenn Sie diese Methoden von außerhalb von Scala aufrufen, deren Problemumgehungen in den Antworten angegeben sind, müssen Sie wissen, welcher implizite Parameter übergeben werden muss, um die gewünschte Methode zu erhalten. Wie unterscheidet sich dies qualitativ von der Notwendigkeit, manuell zu wissen, welcher automatisch entstellte Methodenname aufgerufen werden muss, wenn von außerhalb von Scala aufgerufen wird? Die automatisch entstellten Namen wären viel effizienter und würden all diese Boilerplate eliminieren! Eines Tages werde ich mich darum kümmern, nach einer Scala-Debatte zu fragen.
Shelby Moore III

Antworten:

52

Ich mag die Idee von Michael Krämer, Implizite zu verwenden, aber ich denke, sie kann direkter angewendet werden:

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

Ich denke, das ist gut lesbar und unkompliziert.

[Aktualisieren]

Es gibt noch einen anderen einfachen Weg, der zu funktionieren scheint:

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

Für jede Version benötigen Sie einen zusätzlichen Typparameter, damit dieser nicht skaliert, aber ich denke, für drei oder vier Versionen ist es in Ordnung.

[Update 2]

Für genau zwei Methoden habe ich einen weiteren schönen Trick gefunden:

def foo(list: => List[Int]) = { println("Int-List " + list)}
def foo(list: List[String]) = { println("String-List " + list)}
Landei
quelle
1
Können Sie die X: ClassManifest-Linie näher erläutern?
Mikaël Mayer
1
@ Mikaël Mayer Ich parametrisiere die Methoden mit einem nicht verwendeten Typ X. Da es wirklich nicht verwendet wird, würde dies die Signatur der JVM nicht ändern (aufgrund der Typlöschung). Wenn Sie jedoch Manifeste verwenden, um solche Typen zur Laufzeit zu verfolgen, erhalten Sie intern zusätzliche Argumente für sie, sodass die JVM so etwas "sieht" foo(List l,Classmanifest cx)und sich foo(List l,Classmanifest cx, Classmanifest cy)davon unterscheidet foo(List l).
Landei
Nett! Könnten Sie Ihre erste Lösung, dh basierend auf case classs, mit der von Jean-Philippe Pellet bereitgestellten Lösung vergleichen ?
Daniel
@ Daniel Die Fallklassenlösung erfordert mehr Eingabe, skaliert aber viel besser. Außerdem wird die Länge der Argumentliste nicht erhöht. Meiner Meinung nach ist es weniger "hacky". Der Nachteil ist, dass Sie Ihren Kontext mit impliziten Konvertierungen "verschmutzen", die Anzahl der Klassen erhöhen und Ihr Argument innerhalb der Methode "auspacken" müssen. Ich glaube nicht, dass es zwischen beiden Lösungen große Leistungsunterschiede gibt.
Landei
5
ClassManifestist eine Scala-Lösung vor 2.10, seitdem gibt es TypeTagund ClassTag. Können Sie Ihre Lösung mit PLZ aktualisieren? Weitere Infos: docs.scala-lang.org/overviews/reflection/…
Jules Ivanic
54

Anstatt implizite Dummy-Werte zu erfinden, können Sie die DummyImplicitdefinierten Werte verwenden, in Predefdenen genau das gemacht zu sein scheint:

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) = ()
}
Jean-Philippe Pellet
quelle
1
Funktioniert in meinem Fall nicht. scala: ambiguous reference to overloaded definition, both method apply in class DBObjectExtension of type [A](key: String)(implicit d: DummyImplicit)List[A] and method apply in class DBObjectExtension of type [A](key: String)(implicit d1: DummyImplicit, implicit d2: DummyImplicit)A match argument types (String) and expected result type List[String] val zzzz : List[String] = place("cats") ^Gedanken?
Experte
Ihre beiden Methoden verwenden a Stringals nicht implizites Argument. In meinen Beispielen unterscheidet sich der Typ des Parameters : List[String], List[Int]und List[Date].
Jean-Philippe Pellet
das hat eigentlich nur ein ganz anderes Problem für mich gelöst, danke :)
Erik Kaplun
Nett! Könnten Sie dies mit Landeis Lösung vergleichen, die auf case classs basiert ?
Daniel
Wie welches denkst du ist besser?
Daniel
12

Um die Lösung von Michael Krämer zu verstehen , muss erkannt werden, dass die Typen der impliziten Parameter unwichtig sind. Was ist wichtig, dass ihre Typen unterscheiden.

Der folgende Code funktioniert genauso:

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

Auf Bytecode-Ebene werden beide fooMethoden zu Methoden mit zwei Argumenten, da der JVM-Bytecode keine impliziten Parameter oder mehrere Parameterlisten kennt. Auf der Aufrufseite wählt der Scala-Compiler die entsprechende fooMethode zum Aufrufen (und damit das entsprechende Dummy-Objekt zum Übergeben ) aus, indem er den Typ der übergebenen Liste überprüft (die erst später gelöscht wird).

Dieser Ansatz ist zwar ausführlicher, entlastet den Aufrufer jedoch von der Last, die impliziten Argumente anzugeben. Tatsächlich funktioniert es sogar, wenn die DummyN-Objekte für die TestDoubleDefKlasse privat sind .

Aaron Novstrup
quelle
Wenn ich den DummyN als privat markiere und eine Implementierung in foo-Methoden einfüge, erhalte ich beim Ausführen einen java.lang.NoSuchMethodError.
Oluies
Genius! Ich hatte bereits einen impliziten Parameter in meinem, also kombinierte ich sie:(implicit c: MyContext, d: dummy1.type)
Marcus Downing
Ich finde das die beste Antwort und würde dem ursprünglichen Fragesteller vorschlagen, es noch einmal zu überdenken.
Akauppi
11

Aufgrund der Wunder der Typlöschung werden die Typparameter der Methodenliste während der Kompilierung gelöscht, wodurch beide Methoden auf dieselbe Signatur reduziert werden. Dies ist ein Compilerfehler.

Viktor Klang
quelle
2
Downvote, weil der Autor (zumindest in der aktuellen Form der Frage) anerkennt, dass er den Grund versteht - aber nach besseren Möglichkeiten fragt, dies zu umgehen.
Akauppi
8

Wie Viktor Klang bereits sagt, wird der generische Typ vom Compiler gelöscht. Zum Glück gibt es eine Problemumgehung:

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

Danke für Michid für den Tipp!

Michel Krämer
quelle
7
Dies scheint mir ein schrecklicher Kludge zu sein und die Mühe nicht wert. Ein besserer Kludge (der sich immer noch fraglich lohnt) wäre die Verwendung von Parametern mit Standardwerten, um die beiden Methoden zu unterscheiden.
Tom Crockett
3
@peletom: Ihre Methode (mit Standardparametern) kann nicht mit dem Fehler "Mehrere überladene Alternativen der Methode foo definieren Standardargumente" kompiliert werden.
Ken Bloom
6

Wenn ich kombinieren Daniel s Antwort und Sandor Muraközi Antwort hier erhalte ich:

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

Ich bekomme eine typsichere (ish) Variante

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

Die Logik kann auch als solche in die Typklasse aufgenommen werden (dank jsuereth ): @ annotation.implicitNotFound (msg = "Foo unterstützt nicht nur $ {T} nur Int und String akzeptiert") versiegeltes Merkmal Foo [T] {def apply (Liste: Liste [T]): Einheit}

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)

Welches gibt:

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

Beachten Sie, dass wir schreiben müssen, implicitly[Foo[A]].apply(x)da der Compiler denkt, dass dies implicitly[Foo[A]](x)bedeutet, dass wir implicitlymit Parametern aufrufen .

oluies
quelle
3

Es gibt (mindestens einen) anderen Weg, auch wenn er nicht zu schön und nicht wirklich typsicher ist:

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

Der implizite Manifest-Parameter kann verwendet werden, um den gelöschten Typ zu "reifizieren" und somit das Löschen zu umgehen. Sie können in vielen Blog-Posts, z . B. in diesem, etwas mehr darüber erfahren .

Was passiert ist, dass der Manifest-Parameter Ihnen zurückgeben kann, was T vor dem Löschen war. Dann erledigt ein einfacher Versand basierend auf T an die verschiedenen realen Implementierungen den Rest.

Wahrscheinlich gibt es eine schönere Möglichkeit, den Mustervergleich durchzuführen, aber ich habe ihn noch nicht gesehen. Was die Leute normalerweise tun, ist Matching auf m.toString, aber ich denke, Klassen zu halten ist ein bisschen sauberer (auch wenn es ein bisschen ausführlicher ist). Leider ist die Dokumentation von Manifest nicht zu detailliert, vielleicht hat sie auch etwas, das sie vereinfachen könnte.

Ein großer Nachteil davon ist, dass es nicht wirklich typsicher ist: foo wird mit jedem T zufrieden sein, wenn Sie nicht damit umgehen können, haben Sie ein Problem. Ich denke, es könnte mit einigen Einschränkungen für T umgangen werden, aber es würde es weiter erschweren.

Und natürlich ist das ganze Zeug auch nicht zu schön, ich bin mir nicht sicher, ob es sich lohnt, es zu tun, besonders wenn du faul bist ;-)

Sandor Murakozi
quelle
1

Anstelle von Manifesten können Sie auch Dispatcher-Objekte verwenden, die implizit auf ähnliche Weise importiert wurden. Ich habe darüber gebloggt, bevor Manifeste auftauchten: http://michid.wordpress.com/code/implicit-double-dispatch-revisited/

Dies hat den Vorteil der Typensicherheit: Die überladene Methode kann nur für Typen aufgerufen werden, bei denen Disponenten in den aktuellen Bereich importiert wurden.

michid
quelle
0

Netter Trick, den ich aus http://scala-programming-language.1934581.n4.nabble.com/disambiguation-of-double-definition-resulting-from-generic-type-erasure-td2327664.html von Aaron Novstrup gefunden habe

Dieses tote Pferd noch mehr schlagen ...

Mir ist aufgefallen, dass ein sauberer Hack darin besteht, für jede Methode einen eindeutigen Dummy-Typ zu verwenden, dessen Signatur gelöschte Typen enthält:

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
} 

[...]

Löwe
quelle
2
Ich habe dafür gestimmt, dass ich Aaron Novstrups Antwort ein Duplikat gegeben habe. Ich denke, Sie haben nicht überprüft, ob er hier bereits zwei Jahre vor Ihnen seine Antwort gegeben hat.
Shelby Moore III
0

Ich habe versucht, die Antworten von Aaron Novstrup und Leo zu verbessern, um einen Satz von Standard-Beweisobjekten importierbar und knapper zu machen.

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
}

Dies führt jedoch dazu, dass sich der Compiler beschwert, dass für den impliziten Wert mehrdeutige Auswahlmöglichkeiten bestehen, wenn fooeine andere Methode aufgerufen wird, für die ein impliziter Parameter desselben Typs erforderlich ist.

Daher biete ich nur das Folgende an, was in einigen Fällen knapper ist. Und diese Verbesserung funktioniert mit Wertklassen (denen, die 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
}

Wenn der enthaltende Typname ziemlich lang ist, deklarieren Sie einen inneren trait, um ihn knapper zu machen.

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
}

Wertklassen erlauben jedoch keine inneren Merkmale, Klassen oder Objekte. Beachten Sie daher auch, dass die Antworten von Aaron Novstrup und Leo nicht mit Wertklassen funktionieren.

Shelby Moore III
quelle
-3

Ich habe das nicht getestet, aber warum sollte eine Obergrenze nicht funktionieren?

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

Wechselt die Löschübersetzung zweimal von foo (Liste [Beliebig]) zu foo (Liste [Zeichenfolge]) und foo (Liste [Int] i):

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

Ich glaube, ich habe gelesen, dass in Version 2.8 die Obergrenzen jetzt so codiert sind, anstatt immer ein Any.

Um kovariante Typen zu überladen, verwenden Sie eine invariante Grenze (gibt es eine solche Syntax in Scala? ... ah, ich glaube nicht, aber nehmen Sie Folgendes als konzeptionellen Nachtrag zur obigen Hauptlösung):

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

dann nehme ich an, dass das implizite Casting in der gelöschten Version des Codes beseitigt ist.


UPDATE: Das Problem ist, dass JVM mehr Typinformationen zu Methodensignaturen löscht als "notwendig". Ich habe einen Link bereitgestellt. Es löscht Typvariablen aus Typkonstruktoren, sogar die konkrete Grenze dieser Typvariablen. Es gibt eine konzeptionelle Unterscheidung, da das Löschen des gebundenen Funktionstyps keinen konzeptionellen, nicht reifizierten Vorteil hat, da er zur Kompilierungszeit bekannt ist und sich mit keiner Instanz des Generikums ändert und Anrufer nicht aufrufen müssen Die Funktion mit Typen, die nicht der Typbindung entsprechen. Wie kann die JVM die Typbindung erzwingen, wenn sie gelöscht wird? Gut ein Linksagt, dass der gebundene Typ in Metadaten beibehalten wird, auf die Compiler zugreifen sollen. Dies erklärt, warum die Verwendung von Typgrenzen keine Überladung ermöglicht. Dies bedeutet auch, dass JVM eine weit offene Sicherheitslücke ist, da typgebundene Methoden ohne Typgrenzen aufgerufen werden können (Huch!). Entschuldigen Sie mich, dass die JVM-Designer so etwas unsicheres nicht tun würden.

Als ich das schrieb, verstand ich nicht, dass Stackoverflow ein System ist, bei dem Menschen nach der Qualität der Antworten bewertet werden, wie etwa ein Wettbewerb um den Ruf. Ich dachte, es wäre ein Ort, um Informationen auszutauschen. Zu der Zeit, als ich dies schrieb, verglich ich reifiziertes und nicht-reifiziertes von einer konzeptionellen Ebene (Vergleich vieler verschiedener Sprachen), und daher machte es meiner Meinung nach keinen Sinn, den gebundenen Typ zu löschen.

Shelby Moore III
quelle
1
Wenn Sie dies ablehnen, fügen Sie mir bitte einen Kommentar hinzu oder senden Sie mir einen Kommentar, um dies zu erklären. Ich denke das funktioniert in der neuesten Version? Ich habe es im Moment nicht installiert, um es zu testen. Theoretisch sollte die Obergrenze der Typ sein, auf den gelöscht wird, aber ich denke, dies wurde erst in 2.8 gemacht.
Shelby Moore III
Die Logik, die ich angewendet habe, um anzunehmen, dass dies funktionieren muss, lautet: Wenn die Obergrenze nur mit Umwandlungen innerhalb der Funktion und nicht auf die Signatur der Funktion angewendet würde, würde dies bedeuten, dass die Funktion (von Java) mit einer Typfehlanpassung aufgerufen werden könnte. Oder wenn Sie es vorziehen, verwenden Sie einfach den expliziten existenziellen Typ, den die obere Grenze impliziert, def foo (s: List [_ <: String])
Shelby Moore III
Meine Idee funktioniert möglicherweise nicht, nicht wegen der Typlöschung an sich, sondern weil JVM mehr Typinformationen löscht, als für einen optimalen Betrieb der Typlöschung erforderlich sind. Daher kennt JVM den Unterschied zwischen einer Liste [T] und einer Liste [T anscheinend nicht <: String] Argument (beide sind nur List). In mancher Hinsicht ist das Löschen von Typen vernünftiger / effizienter als das Reifizieren - es wird nur eine Laufzeitversion für alle konkreten Realisierungen benötigt. Um jedoch unnötige Umwandlungen (dh Reflexionen) zu vermeiden, sollte die VM die Grenzen der Typparameter verbreiten und nicht löschen.
Shelby Moore III
1
Dieser Link cakoose.com/wiki/type_erasure_is_not_evil erklärt, dass JVM zu viele Typinformationen verwirft und daher keine optimale Typlöschung implementiert. Meine vorgeschlagene Idee basierte auf der Annahme, dass JVM die Typlöschung optimal durchführt.
Shelby Moore III
3
Man kann sich nicht einfach etwas ausdenken und erwarten, dass man es ernst nimmt und kommentiert. Sie sollten jedoch berücksichtigen, dass die JVM Typvariablen aus Typkonstruktoren löscht. Die richtige Lösung besteht darin, einen Summentyp zu verwenden. Vermeiden Sie immer eine Überlastung in Scala.
Tony Morris