web-dev-qa-db-fra.com

Scala double définition (2 méthodes ont le même type gomme)

J'ai écrit ça en scala et ça ne compilera pas:

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

le compilateur notifie:

[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

Je sais que JVM n’a pas de support natif pour les génériques, je comprends donc cette erreur.

Je pourrais écrire des wrappers pour List[String] et List[Int] mais je suis paresseux :)

Je doute, mais existe-t-il un autre moyen d'exprimer List[String] n'est pas du même type que List[Int]?

Merci.

63
Jérôme

J'aime l'idée de Michael Krämer d'utiliser des implicites, mais je pense qu'elle peut être appliquée plus directement:

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

Je pense que cela est assez lisible et simple.

[Mettre à jour]

Il existe un autre moyen facile qui semble fonctionner:

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

Pour chaque version, vous avez besoin d'un paramètre de type supplémentaire, donc cela ne s'adapte pas, mais je pense que pour trois ou quatre versions, ça va.

[Mise à jour 2]

Pour exactement deux méthodes, j'ai trouvé un autre tour de Nice:

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

Au lieu d'inventer des valeurs implicites factices, vous pouvez utiliser la variable DummyImplicit définie dans Predef qui semble être faite exactement pour cela:

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

En raison des merveilles de l'effacement de type, les paramètres de type de la liste de vos méthodes sont effacés lors de la compilation, réduisant ainsi les deux méthodes à la même signature, ce qui est une erreur du compilateur.

10
Viktor Klang

Pour comprendre la solution de Michael Krämer , il est nécessaire de reconnaître que les types de paramètres implicites importent peu. Ce qui est est important car leurs types sont distincts. 

Le code suivant fonctionne de la même manière:

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

Au niveau du bytecode, les deux méthodes foo deviennent des méthodes à deux arguments puisque le bytecode de la machine virtuelle Java ne connaît rien des paramètres implicites ou des listes de paramètres multiples. Sur le site d'appels, le compilateur Scala sélectionne la méthode foo appropriée à appeler (et donc l'objet factice approprié à transmettre) en examinant le type de la liste transmise (qui n'est effacée que plus tard).

Bien que ce soit plus bavard, cette approche soulage l'appelant de la charge de fournir les arguments implicites. En fait, cela fonctionne même si les objets factices sont privés de la classe TestDoubleDef.

10
Aaron Novstrup

Comme le dit déjà Viktor Klang, le type générique sera effacé par le compilateur. Heureusement, il existe une solution de contournement:

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

Merci pour Michid pour le tuyau!

8
Michel Krämer

Si je combine Daniel s réponse et Sandor Murakozi s réponse ici je reçois: 

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

Je reçois une 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 logique peut également être incluse dans la classe de type en tant que telle (grâce à jsuereth ): @ Annotation.implicitNotFound (msg = "Foo ne prend pas en charge $ {T} uniquement Int et String acceptés") scellé trait Foo [T] {def s’applique (liste: liste [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)

Qui donne: 

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

Notez que nous devons écrire implicitly[Foo[A]].apply(x) car le compilateur pense que implicitly[Foo[A]](x) signifie que nous appelons implicitly avec des paramètres.

6
oluies

Il y a (au moins une) une autre façon, même si ce n'est pas trop gentil et pas vraiment sûr:

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

Le paramenter de manifeste implicite peut être utilisé pour "réifier" le type effacé et éviter ainsi l'effacement. Vous pouvez en apprendre un peu plus à ce sujet dans de nombreux articles de blog, par exemple. celui-là .

Qu'est-ce qui se passe est que le param manifeste peut vous rendre ce que T était avant l'effacement. Ensuite, un simple envoi basé sur T aux différentes mises en œuvre réelles fait le reste.

Il y a probablement une meilleure façon de faire la correspondance de motif, mais je ne l'ai pas encore vue. Ce que les gens font habituellement est d'appariement sur m.toString, mais je pense que garder les classes est un peu plus propre (même si c'est un peu plus détaillé). Malheureusement, la documentation de Manifest n’est pas trop détaillée, elle pourrait peut-être aussi être simplifiée. 

Un gros inconvénient est qu’il n’est pas vraiment sûr de taper: foo sera satisfait de n’importe quel T, si vous ne le supportez pas, vous aurez un problème. Je suppose que cela pourrait être contourné avec certaines contraintes sur T, mais cela compliquerait encore la situation. 

Et bien sûr, tout cela n’est pas trop gentil, je ne suis pas sûr que cela en vaille la peine, surtout si vous êtes paresseux ;-)

3
Sandor Murakozi

Au lieu d'utiliser des manifestes, vous pouvez également utiliser des objets de répartiteurs implicitement importés de la même manière. J'ai blogué à ce sujet avant que des manifestes n'apparaissent: http://michid.wordpress.com/code/implicit-double-dispatch-revisited/

Cela présente l’avantage du type de sécurité: la méthode surchargée ne sera appelable que pour les types dont les régulateurs ont été importés dans la portée actuelle. 

1
michid

J'ai trouvé le bon tour dans http://scala-programming-language.1934581.n4.nabble.com/disambiguation-of-double-definition-resulting-from-generic-type-erasure-td2327664.html par Aaron Novstrup

Battre ce cheval mort un peu plus ... 

Il m'est apparu qu'un hack plus propre consiste à utiliser un type factice unique Pour chaque méthode avec des types effacés dans sa signature:

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
} 

[...]

0
Leo

J’ai essayé d’améliorer les réponses de Aaron Novstrup et de Leo pour rendre un ensemble d’objets de preuve standard importable et plus concis.

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
}

Mais cela entraînera le compilateur à se plaindre de l'existence de choix ambigus pour la valeur implicite lorsque foo appelle une autre méthode nécessitant un paramètre implicite du même type.

Ainsi, je n'offre que ce qui suit, ce qui est plus concis dans certains cas. Et cette amélioration fonctionne avec les classes de valeur (celles qui 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 le nom du type contenant est assez long, déclarez une trait intérieure pour la rendre plus concise.

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
}

Cependant, les classes de valeur n'autorisent pas les traits, classes et objets internes. Notez également que les réponses de Aaron Novstrup et de Leo ne fonctionnent pas avec des classes de valeur.

0
Shelby Moore III