Skip to content
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

Rework variances of higher-kinded types #8082

Merged
merged 32 commits into from
Jan 31, 2020

Conversation

odersky
Copy link
Contributor

@odersky odersky commented Jan 23, 2020

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

  • that are bounds for an abstract type, or
  • that are opaque aliases, or
  • that are match type aliases

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).

@smarter
Copy link
Member

smarter commented Jan 23, 2020

Some related discussions: #6369, #6320

@odersky odersky force-pushed the change-lambda-variance branch 3 times, most recently from d54b1b0 to 7d6e75e Compare January 27, 2020 11:58
@odersky
Copy link
Contributor Author

odersky commented Jan 27, 2020

Some related discussions: #6369, #6320

It turns out neither of these changes with the current PR.

@odersky
Copy link
Contributor Author

odersky commented Jan 27, 2020

todos:

  • drop TYPEALIAS tasty tag
  • simplify pickling of match aliases
  • systematically test all combinations of variances

@odersky odersky marked this pull request as ready for review January 28, 2020 22:18
@odersky
Copy link
Contributor Author

odersky commented Jan 29, 2020

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 `-`.
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)))
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
isOpaqueAlias | params.exists(!_.paramVariance.isEmpty)))
isOpaqueAlias || params.exists(!_.paramVariance.isEmpty)))


## Relationship with Parameterized Type Definitions

type F[X] <: List[F[X]]
Copy link
Member

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
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
```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])(
Copy link
Member

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, _ | _)
Copy link
Member

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.

Copy link
Contributor Author

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
Copy link
Member

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:

Suggested change
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]
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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
Copy link
Member

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)

Suggested change
override val isVariantLambda = variances.nonEmpty
override val isGivenVarianceLambda = variances.nonEmpty

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 changed it to isDeclaredVarianceLambda

case that: HKTypeLambda =>
paramNames.eqElements(that.paramNames)
&& isVariantLambda == that.isVariantLambda
&& (!isVariantLambda
Copy link
Member

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:

Suggested change
&& (!isVariantLambda
&& givenVariances == that.givenVariances

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, 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) =
Copy link
Member

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.

@smarter smarter assigned odersky and unassigned smarter Jan 30, 2020
@@ -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]
Copy link
Contributor

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 typeF2[-A, +B] is known to be contravariant in A and covariant in B

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! Fixed.

Copy link
Member

@smarter smarter left a 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
Copy link
Member

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

@odersky odersky merged commit f833baa into scala:master Jan 31, 2020
@odersky odersky deleted the change-lambda-variance branch January 31, 2020 14:52
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.

Implicit conversion isn't applied for Id type alias Type alias causes problems for implicit search
3 participants