Skip to content

Conversation

@milessabin
Copy link
Contributor

@milessabin milessabin commented Aug 13, 2017

This is a backport of scala/scala3@8954026 to scalac. As in the Dotty implementation the specificity comparison is as-if all contravariant positions in type constructors were covariant. The feature is enabled by specifying -Xsource:3.0.

Initially this broke test/files/run/t2030.scala in exactly the same way as in Dotty as reported by @smarter here. Since then the landing of the new collections has fixed this.

This fixes scala/bug#2509. It also aligns with the contravariant behaviour hoped for in scala/bug#7768, although it differs on the covariant behaviour, which is unchanged. I think that's a reasonable outcome and that scala/bug#7768 can be declared as fixed.

@scala-jenkins scala-jenkins added this to the 2.12.4 milestone Aug 13, 2017
@smarter
Copy link
Member

smarter commented Aug 13, 2017

Nice!

The new specificity behaviour is behind flag -Ydotty-specificity.

I think -Xsource:3.0 was proposed for this sort of things: scala/scala-dev#374

val YdisableFlatCpCaching = BooleanSetting ("-YdisableFlatCpCaching", "Do not cache flat classpath representation of classpath elements from jars across compiler instances.")
val YpartialUnification = BooleanSetting ("-Ypartial-unification", "Enable partial unification in type constructor inference")
val Yvirtpatmat = BooleanSetting ("-Yvirtpatmat", "Enable pattern matcher virtualization")
val YdottySpecificity = BooleanSetting ("-Ydotty-specificity", "Enable Dotty compatible rules for overload selection by specifity")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

selection by specifity")

That's how I usually pronounce it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curs mi speling ...

Copy link
Member

@retronym retronym left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd also like to see how this would be described in the spec, assuming it were to become default behaviour. Sometimes these concepts are easier to describe in comments in the implenentation than they are in the more limited vocabulary of the spec.

val flippedParams = sym.rawInfo.typeParams.map { param =>
val flippedFlags = if(param.isContravariant) (param.flags&(~CONTRAVARIANT))|COVARIANT else param.flags
flippedClassSym.newTypeParameter(param.name.toTypeName, param.pos, flippedFlags) setInfo param.info
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

param.info might refer to the original type parameters, so more substitution is needed. Cloning the class upfront takes care of all the rewiring.

scala> class C[-A <: C[A]]
defined class C

scala> val CClass = symbolOf[C[_]]
CClass: $r.intp.global.TypeSymbol = class C

scala> val CClass_clone = CClass.cloneSymbol(CClass.owner)
CClass_clone: CClass.TypeOfClonedSymbol = class C

scala> CClass_clone.rawInfo.typeParams.foreach(sym => if (sym.isContravariant) sym.resetFlag(Flag.CONTRAVARIANT).setFlag(Flag.COVARIANT))

scala> CClass_clone.defString
res3: String = class C[+A <: C[A]] extends AnyRef

scala> {settings.uniqid.value = true; println(CClass.defString); println(CClass_clone.defString); settings.uniqid.value = false}
class C#132392[-A#132393 <: C#132392[A#132393]] extends AnyRef#1932
class C#145639[+A#145640 <: C#132392[A#145640]] extends AnyRef#1932

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. That simplifies things quite a bit.

val flippedSym =
subst.get(sym) match {
case None =>
val flippedClassSym = sym.owner.newClassSymbol(sym.name.toTypeName, sym.pos, sym.flags)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Surely the new class symbols created by flip will always be treated as unrelated by the subsequent <:<? Maybe that gives the expected result for the test cases.

Dotty's implementation appears to make a type alias instead.

class C[-A]
...
type C'[+A] = C[A] // synthetic (ill-varying) type alias

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that's quite what's going on in Dotty's implementation. In the flip in Dotty the TypeMap traversal appears to traverse through to the type members which correspond to the type params of the TypeRef in the scala implementation. These type members then have their variance individually flipped, via a type alias, as you say. But I think the mechanism in scalac has to be different ... we have to work at the TypeRef/type params level.

On your first point, yes you're right that new symbols will be unrelated by <:< except in the trivial case where the outer type constructor is the same in both cases (here the subst map ensures that the same flipped symbol is used). I'm fairly confident that my implementation here matches the one in Dotty, but it's possible that the Dotty implementation gets this wrong as well.

I think however, that in cases where the outer type constructor differs, we will always want to say that neither is specific as the other. The following is the main problem case (included in the latest commit, Dotty agrees with the implementation here),

trait Z[-T]
trait Quux[-T] extends Z[T]
class A
class B extends A

Given the above, what is the relative specificity of Quux[B] and Z[A]? Both Dotty and this implementation say that neither is as specific as the other even though Z[B] is as specific as Z[A] and Quux[B] is a subtype of Z[B]. @smarter what are your thoughts on this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intuitively I would expect Quux[B] to be more specific than Z[A] for the reasons you gave. The fact that it doesn't in dotty seems like an oversight to me /cc @odersky .

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@retronym is there any way of preserving the subtype relationships of the cloned outer ClassSymbols?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've opened scala/scala3#2974, you might want to weight in on whether you find the top-level-only-flipping semantics sensible or not.

case _ => existentialAbstraction(undef1, tpe1) <:< existentialAbstraction(undef2, tpe2)
case _ =>

if (settings.YdottySpecificity) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This approach looks somewhat expensive, but we could cache the flipped version of each contravariant class symbol by spending a few fields in ClassSymbol.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this also helps.

trait Z[-T]

@RunWith(classOf[JUnit4])
class InferencerTests extends BytecodeTesting {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 for unit tests!

@smarter
Copy link
Member

smarter commented Aug 15, 2017

Could you try making Ordering contravariant on top of this PR ? (cf http://github.com/scala/bug/issues/7179, this might require using @unsafeVariance to deal with Java superclasses)

@milessabin
Copy link
Contributor Author

@smarter aside from the inevitable issues with max and min it looks fine.

@milessabin
Copy link
Contributor Author

Quick and dirty PoC of a contravariant (Partial)Ordering and Equiv here: milessabin@9ca6be8.

I made max and min extension methods of Ordering[T] to avoid the variance issue and didn't bother to handle the Float and Double specializations.

It compiles and partests successfully.

@smarter
Copy link
Member

smarter commented Aug 22, 2017

I made max and min extension methods of Ordering[T] to avoid the variance issue

Would this work?

def max[U <: T](x: U, y: U): U = if (gteq(x, y)) x else y

@milessabin
Copy link
Contributor Author

Would this work?

Yes, but it'd be a bit more work. The nested Ops class interferes.

@milessabin
Copy link
Contributor Author

Latest commit fixes the problem that @retronym pointed out by normalizing the LHS to the type constructor of the RHS via baseType. My reading of the discussion motivating this change is that people's expectations are that specificity runs in the same direction as extension rather than subtyping, so the use of base types at the outer level seems appropriate.

@adriaanm
Copy link
Contributor

I'd prefer to have this target 2.13, as it's about exploring dotty compatibility.

@adriaanm adriaanm modified the milestones: 2.12.4, 2.13.0-M3 Sep 27, 2017
@milessabin
Copy link
Contributor Author

I'm fine with this going into 2.13.x. Would you like me to change -Ydotty-specificity to -Xsource:2.13?

The Dotty bug corresponding to the issue that @retronym raised has been fixed with same test case as included in this PR so we can be reasonably confident that the semantics match: scala/scala3#2974.

@adriaanm
Copy link
Contributor

adriaanm commented Sep 28, 2017

Thanks, yes, let's standardize on -Xsource. Regarding the target of 2.13 v 3.0, we can tweak once it's clear what the impact on user code is.

@ctongfei
Copy link

ctongfei commented Oct 8, 2017

This is great :-) Cats can make all these typeclasses contravariant now!

@milessabin milessabin changed the base branch from 2.12.x to 2.13.x October 17, 2017 14:55
@milessabin
Copy link
Contributor Author

Rebased against 2.13.x ...

@adriaanm where does this leave -Xsource:2.13 ... should I just make this unconditional? Or do we need a -Xsource:dotty? Or something else?

@smarter
Copy link
Member

smarter commented Oct 17, 2017

I would vote for making this unconditional so that it can influence the design of the collection-strawman

@milessabin
Copy link
Contributor Author

I would be very happy to make this unconditional ... @adriaanm?

@adriaanm
Copy link
Contributor

More work is needed before we can get to deciding whether this can go in and which flag (if any) it should be gated by. In short, 1) a correct implementation and 2) a detailed explanation of its pros & cons, including how it affects existing code bases and future ones (the new collections).

First, the code has evolved in dotty since the commit you've ported: https://github.com/lampepfl/dotty/blob/master/compiler/src/dotty/tools/dotc/typer/Applications.scala#L1138. Also, your port of the original commit is not obviously correct to me, and it didn't carry over the nice comment that accompanies the dotty implementation. Can you explain your implementation strategy and why it's different from dotty's?

The higher-level concern is that this illustrates this is evolving in dotty. Are we ready to call the latest approach ready for production? We also need to evaluate how it affects existing code bases. If this helps the new collections design -- great! That's a good motivator and test bed, but it's not a sufficient condition.

@smarter
Copy link
Member

smarter commented Oct 24, 2017

The higher-level concern is that this illustrates this is evolving in dotty.

The only change to the behavior of dotty here was a fix for scala/scala3#2974 and Miles updated his PR to match the agreed-upon behavior. No other changes in this area are planned, though of course we need to see how well this works in practice. I think getting this PR in scalac and seeing what happens on the community build would be very helpful for that.

@adriaanm
Copy link
Contributor

adriaanm commented Oct 25, 2017

Ok, thanks for the background. I'm all for experimentation here, and alignment with dotty, but this is a tricky part of the language and I would like to see a deep dive in how this is implemented and how it will impact existing code.

I still don't understand how the code I linked to and the code in this PR relate. What does FunctionOf do in dotty, and why doesn't it appear here -- is it a cheaper way to flip variance? It would be nice to avoid the symbol cloning (cloning without substitution makes me suspicious, without looking too deeply). Should we do the comparison before or after existential abstraction?

@smarter
Copy link
Member

smarter commented Oct 25, 2017

I still don't understand how the code I linked to and the code in this PR relate. What does FunctionOf do in dotty, and why doesn't it appear here -- is it a cheaper way to flip variance?

Yeah basically you replace all type arguments Foo that appear in contravariant positions by a function type Foo => () to flip their variance, it's kind of a hack :).

@milessabin
Copy link
Contributor Author

The impact will be similar, though not quite as significant, as partial unification ... people have been working around the old specificity behaviour wrt to contravariant type classes in a variety of ad hoc and more or less unsatisfactory ways for years. This change in Dotty and Scala gives people the behaviour they expected all along and makes the workarounds unnecessary.

Existing code will break, as was the case with partial unification, but I think that's a price worth paying, and I'm fairly confident that most people working with contravariant type classes would agree.

@xeno-by
Copy link
Contributor

xeno-by commented Jul 1, 2018

Since we now have a process to discuss changes that narrow the gap between Scala 2 and Scala 3, I suggest we follow this process with this pull request as well. Let's have this change on the agenda of one of the next SIP meetings?

@milessabin
Copy link
Contributor Author

milessabin commented Jul 26, 2018

Rebased, fixed tests broken by 2.13.x deprecations.

More importantly, changed to match the current Dotty semantics which are now to flip variances throughout the type, not just at the top level.

@smarter, I'd welcome your feedback on this.

@adriaanm do you want this behind an -Xsource flag? In which case, what version?

@milessabin
Copy link
Contributor Author

Hey ho ... this should be OK after squashing ...

@milessabin
Copy link
Contributor Author

Squashed.

@adriaanm
Copy link
Contributor

adriaanm commented Aug 3, 2018

My current position is that this should go under -Xsource:3.0. It can then be discussed in a SIP meeting on whether we want this on by default in 2.14. I still think this is a bit of a hack, and the interaction with type inference needs to be fleshed out. See e.g. scala/scala3#4465.

It's too late for this to influence the collections, so I don't think it's urgent from a 2.13 release planning perspective.

@milessabin
Copy link
Contributor Author

If you're happy to merge it with the -Xsource:3.0 logic then I should be able to find time for it next week.

@adriaanm
Copy link
Contributor

adriaanm commented Aug 3, 2018 via email

This is a backport of the following Dotty change to scalac,

scala/scala3@8954026

As in the Dotty implementation the specificity comparison which is used
for overload resolution and implicit selection is now performed as-if
all contravariant positions in type constructors were covariant.

Fixes scala/bug#2509. Fixes scala/bug#7768.
@milessabin
Copy link
Contributor Author

@adriaanm done. Merge when you're happy :-)

I've taken out the spec change for now because having the language conditionalized on the language version was a bit awkward. We should deal with that as part of the SIP for the feature.

@adriaanm adriaanm merged commit cdd6f80 into scala:2.13.x Aug 6, 2018
@SethTisue
Copy link
Member

test/files/run/t7768.scala is failing on Windows. at e.g. https://scala-ci.typesafe.com/view/scala-2.13.x/job/scala-2.13.x-integrate-windows/658/consoleFull we see:

 Inv[A] Inv[B] Inv[C] Inv[D] Inv[E]
-Con[A] Con[A] Con[C] Con[C] Con[E]
-Cov[E] Cov[E] Cov[E] Cov[E] Cov[E]
+java.lang.NoClassDefFoundError: Con
...
+Caused by: java.lang.ClassNotFoundException: Con
...

@milessabin any idea why that might be...?

@adriaanm
Copy link
Contributor

adriaanm commented Aug 8, 2018

🤣 Con is a reserved file name on windows 😱

@SethTisue
Copy link
Member

oh LOL, it must be because con is a reserved name on Windows filesystems?

@SethTisue
Copy link
Member

jinx

SethTisue added a commit to SethTisue/scala that referenced this pull request Aug 8, 2018
CON you call something CON on Windows? No, you CONnot.

references scala#6037
@SethTisue
Copy link
Member

this leaves me puzzled how Shapeless is able to call something Aux, which is also a dirty word in Redmond.

but, a glance at the source shows that Shapeless's Aux is a type alias, and therefore doesn't produce a .class file. This seems in keeping with our suspicions that @milessabin doesn't even exist at runtime.

PR with fix: #7024

@milessabin
Copy link
Contributor Author

Oh. My. Goodness.

@djspiewak
Copy link

Narrator: There comes a moment in everyone's life when they must choose what kind of a person they're going to be. A moment where steel is tempered in the unyielding fires of decision. A moment where all the churning demons of ten thousand unanswered slights rage as one and demand retribution.

In that moment, our hero knew: he wasn't a Windows user.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Contravariance mucks with implicit resolution