Difficulty defining multiple methods with same name

That’s correct. Mapping to JVM methods is just part of the picture (that’s responsible for the overloading issue), though. In general I’d say it’s the mostly direct mapping to the runtime type system that induces type erasure. And one justification for this approach, besides performance, most certainly is more seamless interop with Java libs/code.

1 Like

There’s more affected than just reflection. A simplification of the motivating problem would be this:

trait Foo {
  def bar(f: Int => Int): Unit
  def bar(f: Int => Boolean): Unit
}

The Scala compiler maps compile-time Scala classes/methods to classes/methods in the JVM bytecode representation. The latter doesn’t have a concept of generics, so both methods above (via type erasure) map to

def bar(f: Function): Unit

…and you cannot have two methods with the same signature in the same class.

Which now begs the question why Function[I,O] should suffer from erasure. The answer, I suspect, would use the same argument you used. But Scala is free to rename those (anonymous) functions in the JVM as it wants. In other words it can name it internally different - we would be none the wiser. Same argument for method names. I am assuming we are not inter-operating with any Java code here, but this may not be a restriction.

Note, I am by no means knowledgeable in compilation and the like, but I think Scala does similar things to what I refer to above. I am thinking of traits and multiple inheritance (don’t exist in Java) and companion objects (name clash?). Come to think of it, specialization may also be another example (multiple functions for different types).

So my intuition is that even though the JVM doesn’t doesn’t support generics at the bytecode level, it in fact need not do it. A consistent naming convention may be all that is required. Once again, I am no expert. Just mulling over this issue.

How do you rename an anonymous function? :wink: It’s the type Function[A, B] that’s mapped to a raw type Function at the bytecode level, just as javac would do for Function<A, B> in Java.

scalac theoretically could generate classes IntIntFunction, IntBooleanFunction, etc. instead. But that would trigger lots of other changes in the mapping to JVM bytecode types, it would complicate many things, impact performance (compiler and runtime) and Java interop.

It does, but again, trying to stay close to the given JVM bytecode entities. You certainly could do things differently, but you’d end up with a different language - not necessarily at the syntax level, but definitely in terms of integration with the overall JVM ecosystem.

You might take a look at the discussion in chapter 4 of this thesis, for example. Not sure how close it maps to current Scala compiler techniques - it’s from 2009. But it certainly should give an idea that some thought has been invested into the way Scala types are mapped to the JVM.

I’m not a language designer/compiler expert, but to my understanding the bottom line is that this mostly direct mapping to JVM bytecode constructs is a historic decision at the core of the Scala language. It comes with a couple of downsides, like type erasure or the existence of null, but I trust that the tradeoffs have been weighed carefully.

Agreed. This discussion is basically beating a dead horse: erasure is pretty central to the underlying assumptions of Scala; if you change that, you basically have a different and incompatible language, probably at a more fundamental level than anything happening in Dotty.

(IIRC, this is the most fundamental difference between Java and C#, and greatly influenced the ecosystems there.)

I would say that in comparison to C# the Java and Scala restrictions on overloading are inconveniences rather than serious limitations. Overloading in Java, Scala and C# is done statically, unlike in e.g. Common Lisp (if Wikipedia is correct on multimethods in CL). Therefore relying on overloading for correctness is sometimes a mistake. Look at this C# code:

using System;
 
public class Test
{
	public static void Main()
	{
		object value = "this is a string!";
		print(value); // prints: "Got object!"" instead of "Got string!""
	}
 
	static void print(string value)
	{
		Console.WriteLine("Got string!");
	}
 
	static void print(object value)
	{
		Console.WriteLine("Got object!");
	}
}

https://www.ideone.com/OQRspQ

IIUC Common Lisp would correctly print “Got string!” since it has multimethods.

OTOH in Scala you can emulate multimethods to some extent using pattern matching:

object Main extends App {
	val values: Seq[Any] = Seq(1, "abc", new Object)
	for (a <- values; b <- values) {
		multiMethod(a, b)
	}
 
	def multiMethod(arg1: Any, arg2: Any): Any = {
		(arg1, arg2) match {
			case (x: Int, y: String) => println("Got Int and String!")
			case (x: String, y: Int) => println("Got String and Int!")
			case (x: Int, y: Any) => println("Got Int and Any!")
			case (x: Any, y: Int) => println("Got Any and Int!")
			case _ => println("Got something else!")
		}
	}
}

https://www.ideone.com/ymlLFi

Got Int and Any!
Got Int and String!
Got Int and Any!
Got String and Int!
Got something else!
Got something else!
Got Any and Int!
Got something else!
Got something else!

@tarsa, one huge advantage of multi methods missing in many language attempts to emulate it is that applications can extend the behavior of generic functions without having access to the class definitions. I.e., if you define the behavior of function F which takes multiple arguments. And I define a new class C. I can specify what F does when an instance C is passed as 1st or 2nd or 3rd argument etc. And I can bundle the code in my application, without having to have access to or modify the code which defines F. Moreover, my method on F specializing on C as 3rd argument may call call-next-method to get the otherwise default behavior, and thereafter augment or ignoramus’s its return value.

Admittedly, however, there is a runtime performance penalty for such flexibility, especially if the compiler does not have the option of recompiling the original code in light of the newly added method.

As you see, if you want Lisp’s flexibility in Scala, Java or C# then you’re out of luck anyway. Overloading in such languages adds a lot of complexity to the compiler while giving something unsatisfactory in return.

Strong JIT would probably devirtualize a lot of multi-methods.

2 Likes