-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Rework variances of higher-kinded types #8082
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
f194a86
to
bd5c3b9
Compare
d54b1b0
to
7d6e75e
Compare
todos:
|
caa4d32
to
2e166b7
Compare
I am done with it for now. I am sure this could be improved further spec-wise but I am out of energy and time to do more with it. The PR removes the major sources of ugliness of the previous implementation and solves type inferencing problems in existing code. |
+ refactoring: account for Bivariance + refactpring: collect variance related ops in Variances, move Variances to core.
This is meant as a better alternative to encode variances in parameter names.
Encode them instead in the upper bound lambda of a TypeBounds type. For now, we also encode them in the alias of a Typealias type, but this will be dropped one we pass to structural lambda variance.
The old encoding using semantic parameter name is still in place. The new recording inside TypeBounds exists alongside the old one.
Needs to be passed as a parameter since Namer does not construct a bounds immediately.
These can hopefully be revived once the variance changes have completed
... unless they are on the right hand side of type bounds or match aliases or they are type aliases where some variance is given explicitly with a `+` or `-`.
This reverts commit 7d6e75e.
Type lambdas don't print with variances anymore
Since variances are associated conceptually with higher-kinded type variables, it makes no sense to write them on type lambdas. I believe it's better to disallow writing variances there because it will only need to variance-bike-shedding otherwise.
A refinement type would previously qualify as an `isRef` of Any, AnyKind, or AnyRef. Often that is not what was intended. Break out `isAny`, `isAnyKind`, and `isAnyRef` methods for tests that don't go through refinements.
bounds.derivedAlias(expand(bounds.alias, true)) | ||
case bounds: TypeAlias => | ||
bounds.derivedAlias(expand(bounds.alias, | ||
isOpaqueAlias | params.exists(!_.paramVariance.isEmpty))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isOpaqueAlias | params.exists(!_.paramVariance.isEmpty))) | |
isOpaqueAlias || params.exists(!_.paramVariance.isEmpty))) |
|
||
## Relationship with Parameterized Type Definitions | ||
|
||
type F[X] <: List[F[X]] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Stray line ?
For instance, | ||
```scala | ||
type F2[A, +B] = A => B | ||
```scala |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
```scala | |
``` |
@@ -3457,7 +3443,7 @@ object Types { | |||
* @param resultTypeExp A function that, given the polytype itself, returns the | |||
* result type `T`. | |||
*/ | |||
class HKTypeLambda(val paramNames: List[TypeName])( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation above still says "Variances are encoded in parameter names"
@@ -1951,7 +1933,7 @@ class TypeComparer(initctx: Context) extends ConstraintHandling[AbsentContext] w | |||
val t2 = distributeAnd(tp2, tp1) | |||
if (t2.exists) t2 | |||
else if (isErased) erasedGlb(tp1, tp2, isJava = false) | |||
else liftIfHK(tp1, tp2, op, original) | |||
else liftIfHK(tp1, tp2, op, original, _ | _) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't get why we're doing something different with the variance depending on whether we have an AndType or an OrType, the variance position is the same. If I intersect [+X] => Cov[X]
and [-X] => Contra[X]
I get an invariant type parameter structurally: [X] => Cov[X] & Contra[X]
, but it looks like this code would make the type parameter bivariant instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's because conceptually variances are attached to abstract types, not the lambdas. Say you have two abstract members
trait A { def F[-X] }
trait B { def F[+X] }
object O extends A, B { ... }
Here, the only legal implementation of F
in O
is indeed bivariant.
I have added a comment to the code.
@@ -213,6 +211,10 @@ Standard-Section: "ASTs" TopLevelStat* | |||
OPEN -- an open class | |||
Annotation | |||
|
|||
Variance = SEALED -- invariant |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The implementation actually uses the STABLE flag:
Variance = SEALED -- invariant | |
Variance = STABLE -- invariant |
@@ -5,8 +5,8 @@ | |||
-- Error: tests/neg-custom-args/kind-projector.scala:5:23 -------------------------------------------------------------- | |||
5 |class Bar1 extends Foo[Either[*, *]] // error | |||
| ^^^^^^^^^^^^ | |||
| Type argument Either has not the same kind as its bound <: [_$1] => Any | |||
| Type argument Either has not the same kind as its bound [_$1] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks wrong to me, [_$1]
is not a bound, it's a type parameter list.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's the way we print bounds now. An abstract type
type A >: Nothing <: [X] => X
prints as
type A[X] <: X
Everything after the A
is the bound. I think it's ultimately better this way since it is closer to the source but it might need some fine tuning.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, I think if the error message did not elide the Any and printed ...its bound [_$1] <: Any
it'd be clearer
tparams.head.givenVariance = vs.head | ||
setVariances(tparams.tail, vs.tail) | ||
|
||
override val isVariantLambda = variances.nonEmpty |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would use a more specific name highlighting that this is about lambdas with given variances.
(less important, but it would also be nicer to not overload the term "given", we could say "fixed variance" instead maybe)
override val isVariantLambda = variances.nonEmpty | |
override val isGivenVarianceLambda = variances.nonEmpty |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed it to isDeclaredVarianceLambda
case that: HKTypeLambda => | ||
paramNames.eqElements(that.paramNames) | ||
&& isVariantLambda == that.isVariantLambda | ||
&& (!isVariantLambda |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this line and the next two are equivalent to:
&& (!isVariantLambda | |
&& givenVariances == that.givenVariances |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, but it's faster the way it is written. iso
is performance critical!
case rt => | ||
expand(rt) | ||
def boundsFromParams[PI <: ParamInfo.Of[TypeName]](params: List[PI], bounds: TypeBounds)(implicit ctx: Context): TypeBounds = { | ||
def expand(tp: Type, useVariances: Boolean) = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could be called useGivenVariances
for extra clarity.
ecfeaa7
to
8667689
Compare
@@ -94,6 +87,20 @@ is treated as a shorthand for | |||
```scala | |||
[F >: Nothing <: [X] =>> Coll[X]] | |||
``` | |||
Abstract types and opaque type aliases remember the variances they were created with. So the type | |||
```scala | |||
def F2[-A, +B] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should be type
F2[-A, +B]
is known to be contravariant in A
and covariant in B
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Fixed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Otherwise LGTM!
* Variances are encoded in parameter names. A name starting with `+` | ||
* designates a covariant parameter, a name starting with `-` designates | ||
* a contravariant parameter, and every other name designates a non-variant parameter. | ||
* Variances are encoded in parameter names. A |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like a stray line that wasn't deleted
Decouple variances from type lambdas. Normally, a type lambda is just a function from types to types, it does not have a declared parameter variance. Of course, the variance of a type parameter can be determined by tracking occurrences of the parameter on the right hand side. That's a structural criterion, not
a user-defined one.
Instead of associating variances with all type lambdas, associate them only with those type lambdas
So conceptually, the variance goes with the type alias or abstract type, not with the type lambda. In the current implementation, variances are still stored in type lambdas since it turned out to be simpler that way. But those lambdas are now classified as variant lambdas (i.e. those that are bounds or right hand sides of type declarations of the kinds listed above) and other lambdas. For those other lambdas, determine the parameter variances structurally, by looking at parameter occurrences on the right hand side.
Fixes #7993
Fixes #8049
Note: In the Type data structure, variances are stored in lambdas. But in the Tasty format they are associated with TYPEBOUNDS tags.
Note: This PR also drops the kludge of encoding lambda variances in parameter names (formerly of kind VariantName).