Scala double definition (2 methods have the same type erasure)

16,005

Solution 1

I like Michael Krämer's idea to use implicits, but I think it can be applied more directly:

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

I think this is quite readable and straightforward.

[Update]

There is another easy way which seems to work:

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

For every version you need an additional type parameter, so this doesn't scale, but I think for three or four versions it's fine.

[Update 2]

For exactly two methods I found another nice trick:

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

Solution 2

Instead of inventing dummy implicit values, you can use the DummyImplicit defined in Predef which seems to be made exactly for that:

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

Solution 3

To understand Michael Krämer's solution, it's necessary to recognize that the types of the implicit parameters are unimportant. What is important is that their types are distinct.

The following code works in the same way:

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

At the bytecode level, both foo methods become two-argument methods since JVM bytecode knows nothing of implicit parameters or multiple parameter lists. At the callsite, the Scala compiler selects the appropriate foo method to call (and therefore the appropriate dummy object to pass in) by looking at the type of the list being passed in (which isn't erased until later).

While it's more verbose, this approach relieves the caller of the burden of supplying the implicit arguments. In fact, it even works if the dummyN objects are private to the TestDoubleDef class.

Solution 4

Due to the wonders of type erasure, the type parameters of your methods' List get erased during compilation, thus reducing both methods to the same signature, which is a compiler error.

Solution 5

As Viktor Klang already says, the generic type will be erased by the compiler. Fortunately, there's a workaround:

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

Thanks for Michid for the tip!

Share:
16,005
Jérôme
Author by

Jérôme

Updated on June 02, 2022

Comments

  • Jérôme
    Jérôme almost 2 years

    I wrote this in scala and it won't compile:

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

    the compiler notify:

    [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
    

    I know JVM has no native support for generics so I understand this error.

    I could write wrappers for List[String] and List[Int] but I'm lazy :)

    I'm doubtful but, is there another way expressing List[String] is not the same type than List[Int]?

    Thanks.

  • Tom Crockett
    Tom Crockett almost 14 years
    This seems like a horrible kludge to me, and not worth the effort. A better kludge (still questionably worth it) would be to use parameters with default values to differentiate the two methods.
  • Ken Bloom
    Ken Bloom almost 14 years
    @peletom: your method (of default parameters) fails to compile with the error "multiple overloaded alternatives of method foo define default arguments."
  • oluies
    oluies over 13 years
    If I mark the dummyN as private and put an implementation in foo methods I get an java.lang.NoSuchMethodError when executing them.
  • Shelby Moore III
    Shelby Moore III about 13 years
    If you vote this down, kindly add or send me a comment to explain. I think this works in the latest version? I don't have it installed at the moment to test. In theory the upper bound, should be the type erased to, but I think this was only done in 2.8.
  • Shelby Moore III
    Shelby Moore III about 13 years
    The logic I applied to assume that this must work, is that if the upper bound was only applied with casts inside the function and not to the function's signature, then it would mean the function could be called (by Java) with a type mismatch. Or if you prefer just use the explicit existential type, which afaics is what the upper bound implies, def foo(s: List[_ <: String])
  • Shelby Moore III
    Shelby Moore III about 13 years
    My idea might not work, not because of type erasure per se, but because JVM erases more type information than is necessary for optimum operation of type erasure, thus apparently JVM does not know the difference between a List[T] and a List[T <: String] argument (both are just List). In some respects, type erasure is more sane/efficient than reification-- need only 1 runtime version for all concrete realizations, but to avoid unnecessary casts (i.e. reflection), the VM should propagate and not erase the bounds of the type parameters.
  • Shelby Moore III
    Shelby Moore III about 13 years
    This cakoose.com/wiki/type_erasure_is_not_evil link explains that JVM is discarding too much type information, and thus does not implement optimum type erasure. My suggested idea was based on the assumption that JVM was doing type erasure optimally.
  • Tony Morris
    Tony Morris about 13 years
    You can't just make stuff up and expect to be taken seriously and with commentary. Nevertheless, you might want to consider the fact that the JVM erases type variables from type constructors. The correct solution is to use a sum type. Avoid overloading in Scala, always.
  • Marcus Downing
    Marcus Downing over 12 years
    Genius! I already had an implicit parameter in place on mine, so I combined them: (implicit c: MyContext, d: dummy1.type)
  • Shelby Moore III
    Shelby Moore III over 12 years
    Tony Morris, you apparently didn't bother to read my comment immediately above yours. The problem is that JVM erases more type information on method signatures than is "necessary". I provided a link. It erases type variables from type constructors, even the concrete bound of those type variables. There is a distinction, because there is no advantage to erasing the type bound, as it is known at compile-time and does not vary with any instance of the generic.
  • Blaisorblade
    Blaisorblade over 12 years
    It's obvious that "the JVM erases more type information on method signatures than is "necessary"". C# is implemented without erasure, and there the above problem does not show up; Java could be implemented without erasure as well - the only reason for having erasure in Java was backward compatibility. Finally, erasure is not performed by the JVM but by compilers producing Java bytecode.
  • expert
    expert about 11 years
    Doesn't work in my case. 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") ^ Thoughts?
  • Jean-Philippe Pellet
    Jean-Philippe Pellet about 11 years
    Your two methods both take a String as the non-implicit argument. In my examples, the parameter's type is different: List[String], List[Int] and List[Date].
  • Mikaël Mayer
    Mikaël Mayer over 10 years
    Can you explain more the X:ClassManifest line ?
  • Landei
    Landei over 10 years
    @Mikaël Mayer I parametrize the methods with an unused type X. Because it is really unused, this wouldn't change the signature as seen by the JVM (because of type erasure). However if you use manifests in order to track such types at runtime, you get internally additional arguments for them, so the JVM "sees" something like foo(List l,Classmanifest cx) and foo(List l,Classmanifest cx, Classmanifest cy) which is different from foo(List l).
  • Shelby Moore III
    Shelby Moore III over 9 years
    I downvoted for providing a duplicate to Aaron Novstrup’s answer. I guess you did not check to see he already provided his answer here two years before you did.
  • Erik Kaplun
    Erik Kaplun about 9 years
    this actually just kind of solved a whole different problem for me, thanks :)
  • Daniel
    Daniel over 8 years
    Nice! Could you compare this to Landei's solution based on case classs?
  • Daniel
    Daniel over 8 years
    Like which one do you think is better?
  • Daniel
    Daniel over 8 years
    Nice! Could you compare your first solution, i.e. based on case classs with the solution provided by Jean-Philippe Pellet?
  • Landei
    Landei over 8 years
    @Daniel The case class solution requires more typing, but scales much better. Further, it doesn't increase the length of the argument list. In my opinion it is less "hacky". The downside ist that you "pollute" your context with implicit conversions, increase the number of classes, and that you need to "unwrap" your argument inside the method. I don't think that there are major performance differences between both solutions.
  • Jean-Philippe Pellet
    Jean-Philippe Pellet over 8 years
    I'd argue this one is more idiomatic. But I'm biased; I proposed this answer.
  • Jeroen Rosenberg
    Jeroen Rosenberg about 8 years
    Nice solution, although it makes testing a bit more difficult. Especially when using mocks, since you practically have an additional parameter to mock and verify against.
  • Jules Ivanic
    Jules Ivanic about 8 years
    ClassManifest is a pre-2.10 Scala solution, since then there is TypeTagand ClassTag. Can you update your solution with plz ? More infos: docs.scala-lang.org/overviews/reflection/…
  • akauppi
    akauppi almost 8 years
    Downvote, because the author (at least in the current form of the question) acknowledges they understand the reason - but ask for better ways around it.
  • akauppi
    akauppi almost 8 years
    I find this the best answer, and would suggest the original question asker to reconsider.
  • EECOLOR
    EECOLOR almost 8 years
    "For exactly two methods I found another nice trick" - be aware that this changes the semantics. The use of a 'by-name' parameter causes it to be evaluated each time it's used.