scala3
scala3 copied to clipboard
Spurious 'type is not a member'
Compiler version
3.0.0 - 3.2.0-RC1-bin-20220511-7c446ce-NIGHTLY
Minimized code
trait DomainIdProvider[T] {
type Id = List[T]
}
object Country extends DomainIdProvider[Country]
case class Country(
id: Country.Id,
)
Output
type Id is not a member of object Country
Explanation
This always compiles fine on 2.13 and sometimes on 3. I couldn't find the exact rule when it compiles and when it doesn't. In the example above it would compile fine if i move case class definition before object, but in other cases this doesn't help.
but in other cases this doesn't help
@OlegYch not sure if I got this right. Do you mean there are some cases when changing the order of declaration of a class and its companion doesn't make the code compile? Could you give an example then?
@prolativ yep that's what i mean i couldn't minimise it though
sometimes it would compile incrementally, but fail after clean
If you have any unminimized examples, that would be helpful too anyway
case class Province(code: Province.Id, symbol: String)
object Province extends DomainIdProvider[Province]
for example this compiles incrementally, but doesn't after clean
I haven't managed to reproduce the problem for this case ... Do you put
trait DomainIdProvider[T] {
type Id = List[T]
}
in another file and then try to compile both files with a single scalac
command?
@prolativ yep
any news on this?
a bit simpler repro:
class X[T] {
type Id = T
}
object A extends X[B]
class B(id: A.Id)
still an issue on 3.2.1-RC1-bin-20220723-1b299b3-NIGHTLY
a workaround is to delete type parameter...
class X {
type Id
}
object A extends X {
type Id = B
}
class B(id: A.Id)
still an issue on 3.2.2
Dale points out that A
and B
mutually refer to each other — more specifically, A.Id
and B
mutually refer to each other – and such circular structures are always a danger area for being dependent on order of compilation — either across multiple files, or the order of definitions within a single file
as in, this compiles! (just swapping the order of A
and B
):
class X[T] {
type Id = T
}
class B(id: A.Id)
object A extends X[B]
yes, it is pretty random
I ran into the same problem in #16924, because there's a test case in the repo that has a somewhat similar situation, where if one file is typed before the other, with my change you end up querying the children of a top level sealed trait before typing and discovering a nested second child... This issue seems like the easier version of the problem to study.
an alternate form that also fails:
class X[T] { trait Id }
object A extends X[B]
class B(id: A.Id)
this seems slightly simpler to me — it shows that we need a type parameter (T
), and we need an inherited member (Id
), but the two needn't be connected to each other
it's unfortunate that the compiler feels it needs to process B's constructor so early. at the time A first mentions B, we need to enter B, but can we put off looking at B's constructor until later?
Dale has found that the isSameKindAs
check seems be the culprit — that check needs to be done more gently
a user on Discord hit this today — their version looks like https://scastie.scala-lang.org/KmklIKFYTmKzmeEgwNQfuA
that check needs to be done more gently
or more kindly
So, we can short-circuit the kindness check for the Oleg's minimisation:
+ val selfParams = self.typeParams
+ val otherParams = other.typeParams
+ if selfParams.isEmpty && otherParams.isEmpty then true // defer calling hkResult to avoid completing types
+ else
val selfResult = self.hkResult
val otherResult = other.hkResult
But then what about the same case, with only the change that the referenced type actually is higher-kinded?
// like tests/pos/i15177.scala but with B being higher kinded
class X[T[_]] {
type Id
}
object A extends X[B]
class B[C](id: A.Id)
Well, before we even get to doing the kind checks for A
we need to check/make X[B]
legal, which it only is if we can eta-expand B
such that it fits T[_]
, and that requires completing B
.
So, then, why does completing B
require completing B
primary constructor? Well, if I'm understanding this right, I think that's required because constructor parameters can be a part of the parent type:
/** Ensure constructor is completed so that any parameter accessors
* which have type trees deriving from its parameters can be
* completed in turn. Note that parent types access such parameter
* accessors, that's why the constructor needs to be completed before
* the parent types are elaborated.
*/
def completeConstructor(denot: SymDenotation): Unit = {
So should we fix it for non-higher kinded types only? I think that would fix the FakeEnum case too.
@smarter wdyt? Should we try and allow some of this? I have a patch that makes some attempts at progress for the applied type case (object A extends X[B[D]]
) but it's incomplete. And perhaps there's some way to do it for eta-expanding too, maybe.
I've been thinking on this. Naturally we can't in general promise to support absolutely everything that compiled in Scala 2. But we should default to trying to, within reason, depending on context. And here the context is a code pattern that looks simple enough and reasonable enough to me, plus it's been hit by multiple users, that I think we should try for backward compatibility for the known use cases, even if we don't have a fully general solution.
I think that's required because constructor parameters can be a part of the parent type
They can be part of the parent tree (class Foo(x: Int) extends Bla(x)
) but they're no longer allowed to be part of the parent type (class Foo(x: Int) extends Bla[x.type]
) as of https://github.com/lampepfl/dotty/pull/16664, so maybe the completion logic can be tweaked?