Okay, let's break down why this happens in Scala 3 (and largely in Scala 2 as well). The core reason lies in the distinction between name resolution and overloading resolution.
-
Name Resolution First: When the compiler encounters a name like
normalMeth
, its first job is to figure out what that name refers to in the current scope. It looks at:- Local definitions
- Members of the enclosing class/object/trait
- Inherited members
- Imported names
-
Imports Bring Names:
import A.*
brings the namenormalMeth
(which refers specifically to the methodA.normalMeth
) into the current scope.import B.*
also brings the namenormalMeth
(which refers specifically to the methodB.normalMeth
) into the current scope.
-
Ambiguity at the Name Level: Now, the compiler sees the simple name
normalMeth
and finds two distinct definitions for it in the current scope, imported from different origins (A
andB
). At this stage, the compiler doesn't even look at the arguments yet. It simply sees that the namenormalMeth
is ambiguous because it could refer toA.normalMeth
orB.normalMeth
. This is a name collision. -
Overloading Resolution Later: Overloading resolution only happens after the compiler has successfully resolved a name to a single entity (like a class or object) that potentially contains multiple methods with that same name.
- When you define overloaded methods together within the same class or object (let's say
object C
), like this:Here, the nameobject C { def normalMeth(foo: Foo): Foo = foo def normalMeth(bar: Bar): Bar = bar } import C.* // Or just C.normalMeth(...)
normalMeth
unambiguously resolves to the set of methods defined withinC
. Only then does the compiler look at the arguments (new Foo
) and use overloading resolution rules to pick the best match (C.normalMeth(foo: Foo)
) from that specific set.
- When you define overloaded methods together within the same class or object (let's say
Analogy:
Imagine you have two phone books, one from Town A and one from Town B.
import A.*
is like opening the Town A phone book.import B.*
is like opening the Town B phone book.
If both phone books have an entry for "John Smith" (but they are different people with different numbers), and you just ask someone to "Call John Smith", they will ask "Which John Smith? The one from Town A or the one from Town B?". That's the ambiguity.
If, however, you have a single phone book for your combined town (like object C
), and it lists two "John Smiths" with different details (like different departments), when you say "Call John Smith in accounting", the operator can look at the details (the arguments/context) to figure out which one you mean.
Scala 3's Design Philosophy:
Scala 3 generally aims for simpler and more predictable scoping rules compared to some of the complexities in Scala 2 (especially around implicits and package objects). Explicitly importing two different things under the same simple name leads to a clear name collision, which the compiler flags as an error rather than trying complex heuristics to guess if they should form an overload set. Overloading is considered a property of methods defined within the same entity.
How to Resolve:
- Use Qualified Names: Be explicit about which one you want.
A.normalMeth(new Foo) B.normalMeth(new Bar)
- Rename on Import: Give one or both imported methods a different local name.
import A.normalMeth as normalMethA import B.normalMeth // Keep B's as normalMeth // or import B.normalMeth as normalMethB normalMethA(new Foo) normalMeth(new Bar) // If B's wasn't renamed // or normalMethB(new Bar) // If B's was renamed
- Consolidate Definitions: If it makes sense logically, put the related methods together in a single object or trait. This is often the cleanest solution if the methods represent variations of the same core concept.
In summary: Imports bring names into scope. If the same name is imported from multiple sources, it's a name collision before overloading resolution even gets a chance to run. Overloading resolution only applies to multiple methods sharing the same name within the same defining entity.