Difficulty defining multiple methods with same name

I have another confusing issue similar to the one reported here: function already defined

Can someone help me understand the limitation?

I have three definitions of a method binSearch, each with different types. Both IntelliJ and the scala compiler complain about one of these but neither of the others. Interesting, InteliJ and scalac give different errors. Here are the definitions.

object BinarySearch {

  def binSearch(left:Double, right:Double, f:Double=>Double, target:Double, threshold:Double,maxDepth:Int):Option[Double] = {
    binSearch(left, right, x=>f(x)-target, threshold,maxDepth)
  }
  def binSearch(left:Double, right:Double, f:Double=>Double, threshold:Double ,maxDepth:Int):Option[Double] = {
    ???
  }

  def binSearch(left:Double, right:Double, f:Double=>Boolean, delta:Double, maxDepth:Int):Option[Double] = {
    // takes a function, f, for which f(left) = false, and f(right) = true,
    // finds an x such that f(x)=false, and f(x+threshold)= true
    ???
  }

  def main(argv:Array[String]) = {

  }
}

IntelliJ complains that the call to binSearch on line 4:

whereas, scalac, complains about a double definition.

Error:(15, 7) double definition:
def binSearch(left: Double,right: Double,f: Double => Double,threshold: Double,maxDepth: Int): Option[Double] at line 11 and
def binSearch(left: Double,right: Double,f: Double => Boolean,delta: Double,maxDepth: Int): Option[Double] at line 15
have same type after erasure: (left: Double, right: Double, f: Function1, threshold: Double, maxDepth: Int)Option
  def binSearch(left:Double, right:Double, f:Double=>Boolean, delta:Double, maxDepth:Int):Option[Double] = {

This time compiler message tells it straight. Both arguments f: Double => Double and f: Double => Boolean are erased to the same thing f: Function1. You can’t define two methods of the same name that have the same parameter types after erasure.

f: A => B is syntactic sugar for f: Function1[A, B]. Erasure removes generic parameters so you’re left with f: Function1.

4 Likes

Ouch. So by that I can’t have two methods one for List[String] and the other for List[Int]? Isn’t this a serious limitation? Well I guess I don’t have to like it; I just have to understand it.

1 Like

Only overloading a method has this limitation. If you give up on overloading methods, i.e. if you start naming them differently, then the problem will be gone.

In my opinion, overloading frequently makes code less readable if parameters (of the same index in parameters list) are used for different purposes.

In this case instead of three methods named binSearch I would opt for method names like byPredicate, byNumber, etc and thus avoid overloading.

If you really want to do overloading here then you can e.g. combine it with default parameters. Then you’re left with:

  def binSearch(left:Double, right:Double, f:Double=>Double, threshold:Double,maxDepth:Int, target:Double = 0.0):Option[Double]

  def binSearch(left:Double, right:Double, f:Double=>Boolean, delta:Double, maxDepth:Int):Option[Double]

…and the compilation error should be gone, because you have different erased signatures (first one has one more parameter).

1 Like

This is a really general hard limitation, and I strongly recommend getting it internalized – type parameters are erased at runtime, which means the resulting classes look alike. So you can’t use that for overloading, you can’t use it for pattern-matching, you can’t use it for isInstanceOf, etc. This is one of the reasons I recommend learning typeclasses, since the typeclass pattern is less constrained in this way.

But for this particular example, I agree with @tarsa: the problem here is really overloading. This is one of the many reasons why many of us tend to avoid overloading, and just use different names…

2 Likes

I agree - serious limitation. This erasure thingy is really annoying for me. 8-(

1 Like

As a side note, type erasure is not a Scala but rather a JVM limitation…

I avoid overloading, except maybe in the outermost layer of an API where it might benefit end users. But having said that, there’s a trick you can use. There is a thing called DummyImplicit which has a value that’s always available implicitly, and you can add it to change a method’s arity so you can distinguish methods that would otherwise be identical due to erasure.

For example, this compiles and you can call all three methods normally, but only because the methods actually differ in arity. If you remove the implicit arg lists it won’t compile anymore.

def foo(as: List[Int]) = as.length
def foo(as: List[String])(implicit ev: DummyImplicit) = 42
def foo(as: List[Boolean])(implicit ev1: DummyImplicit, ev2: DummyImplicit) = -1


foo(List(1, 2))
foo(List("foo", "bar"))
foo(List(true))

I can’t really recommend using this as a matter of practice but it can get you out of a pinch.

3 Likes

Another workaround that I sometimes use is adding an extra parameter with a default value, such as dummy: Null = null.

1 Like

Overloading is a natural fit here. For instance, look at BigDecimal. Btw., within object BinarySearch you usually would not repeat the name of the object in method names but use def apply instead.

Now, why to pollute your signatures by unnecessary parameters?

One more approach to get rid of type erasure issues is to improve your parameter types. If both target and threshold are doubles, think about wrapping them in classes like case class Threshold(value: Double). You may also define your own classes for functions like

case class F(f: Double => Boolean) {
  def apply(d: Double): Boolean = f(d)
}

To add ByX-suffixes to the method names as suggested by Piotr is of course also a valid workaround. It is mostly adequate in case the methods do slightly different things.

Use default parameter values as suggested by Naftoli if you don’t really have distinct parameters but your implementation would set them to default values.

@jducoeur, can you explain what this limitation really means. What is it that is disallowed in overloading and pattern matching? If types are erased at compile time, what does that imply about what I can define and not define.
For example, apparently I am allowed to define one method whose argument is Double and another method of the same name whose argument is Int. But not two whose arguments are Int=>Double and Int=>Int respectively? Why does erasure only make one of these forms illegal?

Generics - it’s Int/Double vs Function[Int, Double]/Function[Int, Int]. After type erasure, you just have raw Function twice in the second case.

@sangamon, can you explain more explicitly what you mean? Are you saying that type erasure means not that types are erased as the term implies, but rather that (and only that) type parameters to generic classes are erased? Implying that no run-time decision can be made based on a type parameter to a generic class.

Curiously, when I look at the Scala Generics documentation page, there’s no link to erasure. Is this an oversize, or should I reasonably expect there be some mention there?

And when I search for erasure, I see lots of blogging, but no definitive explanation in the documentation and how it relates to Generics.

Is this explained in the documentation, or am I not long finding the correct page?

This is not the documentation page on generics, but part of the “Tour” - “bite-sized introductions to core language features”. I wouldn’t expect type erasure to be covered there.

Type erasure is specified, though not rationalized, in the language spec (§5.1.3). It is not a Scala language “feature”, but a constraint imposed by the underlying JVM runtime type system. There are some Scala-specific mechanisms to soften the blow for some use cases. For overloading, though, the best solution IMHO simply is: don’t do it.

This blog post looks like a useful introduction to type erasure.

Best regards,
Patrick

Type erasure applies only to generics. It’s purpose is mainly to achieve high degree of compatibility between code not using generics and code using generics. Example in Java:

import java.util.ArrayList;
import java.util.List;

public class Cooperation {
    public static void main(String[] args) {
        List rawList = new ArrayList();
        List<String> genericList = rawList;
        rawList.add("abc");
        System.out.println(genericList.get(0)); // prints: abc
        genericList.add("xyz");
        System.out.println(rawList.get(1)); // prints: xyz
    }
}

Erasure of non-generic types was never needed for migration of old codebases to newer Java versions, so it’s not done.

You really need to get a better documentation source - the Tour just is a high-level overview of the high points. It isn’t detailed documentation, nor is it intended to be. This sort of thing tends to be discussed in more detail in the real docs and books.

But yeah - erasure applies only to type parameters, all of which basically get squashed out so they don’t really exist at runtime. So anything that needs to make decisions at runtime can’t use them. (Which is one reason why compile-time mechanisms like typeclasses are much more powerful.)

And yes, that takes some getting used to, but it’s largely out of our control, since it is a platform limitation.

This somehow does not seem a good reason. I don’t have a example handy but I have already had this erasure issue in Scala code that does not interface with any Java code. Should’t this problem only be an issue when we use Java data structures?

It’s always an issue because Scala is compiled to Java bytecode, running on the JVM and mapping to its runtime type system that doesn’t have a concept of generics.

To be pedantic, isn’t it true that the reason is not that Scala compiles to JVM, but that Scala implements its methods as java methods. Conceivably a JVM based language might choose to implement its methods otherwise, not as Java methods, and thus allow a more expressive type system than Scala, with a potentially huge sacrifice in performance?

Actually I have an intuition that the type system and compilation to bytecode (in whatever form) are independent - we only need type information when compiling (Scala only). Of course this means no reflection (I can live with this), think that is what @sangamon is referring to. I am under the impression that Dotty actually generates and stores this type information during its compilation process and “carries it around” in the compiled code. I also recall work on TASTY, but the description I see, only mentions reflection.

I keep harping on this because I used OCaml for a few years and never had to deal with erasure. Miss that.

Any experts out there to enlighten me?