Skip to content

Commit 2dd964d

Browse files
authored
Merge pull request #5217 from dotty-staging/spec-type-lambdas
Type Lambdas Spec
2 parents d3a0ac8 + 3bb33a9 commit 2dd964d

File tree

9 files changed

+193
-3
lines changed

9 files changed

+193
-3
lines changed

compiler/src/dotty/tools/dotc/transform/PostTyper.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ package transform
44
import dotty.tools.dotc.ast.{Trees, tpd, untpd}
55
import scala.collection.mutable
66
import core._
7-
import typer.Checking
7+
import typer.{Checking, VarianceChecker}
88
import Types._, Contexts._, Names._, Flags._, DenotTransformers._, Phases._
99
import SymDenotations._, StdNames._, Annotations._, Trees._, Scopes._
1010
import Decorators._
@@ -296,6 +296,9 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase
296296
// when trying to typecheck self types which are intersections.
297297
Checking.checkNonCyclicInherited(tree.tpe, tree.left.tpe :: tree.right.tpe :: Nil, EmptyScope, tree.pos)
298298
super.transform(tree)
299+
case tree: LambdaTypeTree =>
300+
VarianceChecker.checkLambda(tree)
301+
super.transform(tree)
299302
case Import(expr, selectors) =>
300303
val exprTpe = expr.tpe
301304
val seen = mutable.Set.empty[Name]

compiler/src/dotty/tools/dotc/typer/VarianceChecker.scala

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import Types._, Contexts._, Flags._, Symbols._, Trees._
77
import Decorators._
88
import Variances._
99
import NameKinds._
10+
import TypeApplications.varianceConforms
1011
import util.Positions._
1112
import config.Printers.variances
1213
import reporting.trace
@@ -19,6 +20,41 @@ object VarianceChecker {
1920
case class VarianceError(tvar: Symbol, required: Variance)
2021
def check(tree: tpd.Tree)(implicit ctx: Context): Unit =
2122
new VarianceChecker()(ctx).Traverser.traverse(tree)
23+
24+
/** Check that variances of type lambda correspond to their occurrences in its body.
25+
* Note: this is achieved by a mechanism separate from checking class type parameters.
26+
* Question: Can the two mechanisms be combined in one?
27+
*/
28+
def checkLambda(tree: tpd.LambdaTypeTree)(implicit ctx: Context): Unit = tree.tpe match {
29+
case tl: HKTypeLambda =>
30+
val checkOK = new TypeAccumulator[Boolean] {
31+
def error(tref: TypeParamRef) = {
32+
val VariantName(paramName, v) = tl.paramNames(tref.paramNum).toTermName
33+
val paramVarianceStr = if (v == 0) "contra" else "co"
34+
val occursStr = variance match {
35+
case -1 => "contra"
36+
case 0 => "non"
37+
case 1 => "co"
38+
}
39+
val pos = tree.tparams
40+
.find(_.name.toTermName == paramName)
41+
.map(_.pos)
42+
.getOrElse(tree.pos)
43+
ctx.error(em"${paramVarianceStr}variant type parameter $paramName occurs in ${occursStr}variant position in ${tl.resType}", pos)
44+
}
45+
def apply(x: Boolean, t: Type) = x && {
46+
t match {
47+
case tref: TypeParamRef if tref.binder `eq` tl =>
48+
val v = tl.typeParams(tref.paramNum).paramVariance
49+
varianceConforms(variance, v) || { error(tref); false }
50+
case _ =>
51+
foldOver(x, t)
52+
}
53+
}
54+
}
55+
checkOK.apply(true, tl.resType)
56+
case _ =>
57+
}
2258
}
2359

2460
class VarianceChecker()(implicit ctx: Context) {
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
---
2+
layout: doc-page
3+
title: "Type Lambdas - More Details"
4+
---
5+
6+
## Syntax
7+
8+
```
9+
Type ::= ... | HkTypeParamClause ‘=>’ Type
10+
HkTypeParamClause ::= ‘[’ HkTypeParam {‘,’ HkTypeParam} ‘]’
11+
HkTypeParam ::= {Annotation} [‘+’ | ‘-’] (Id[HkTypeParamClause] | ‘_’) TypeBounds
12+
TypeBounds ::= [‘>:’ Type] [‘<:’ Type]
13+
```
14+
15+
### Type Checking
16+
17+
A type lambda such `[X] => F[X]` defines a function from types to types. The parameter(s) may carry bounds and variance annotations.
18+
If a parameter is is bounded, as in `[X >: L <: H] => F[X]` it is checked that arguments to the parameters conform to the bounds `L` and `H`.
19+
Only the upper bound `H` can be F-bounded, i.e. `X` can appear in it.
20+
21+
A variance annotation on a parameter indicates a subtyping relationship on type instances. For instance, given
22+
```scala
23+
type TL1 = [+A] => F[A]
24+
type TL2 = [-A] => F[A]
25+
```
26+
and two types `S <: T`, we have
27+
```scala
28+
TL1[S] <: TL1[T]
29+
TL2[T] <: TL2[S]
30+
```
31+
It is checked that variance annotations on parameters of type lambdas are respected by the parameter occurrences on the type lambda's body.
32+
33+
**Note** No requirements hold for the variances of occurrences of type variables in their bounds. It is an open question whether we need to impose additional requirements here
34+
(`scalac` doesn't check variances in bounds either).
35+
36+
## Subtyping Rules
37+
38+
Assume two type lambdas
39+
```scala
40+
type TL1 = [v1 X >: L1 <: U1] => R1
41+
type TL2 = [v2 X >: L2 <: U2] => R2
42+
```
43+
where `v1` and `v2` are optional variance annotations: `+`, `-`, or absent.
44+
Then `TL1 <: TL2`, if
45+
46+
- the type interval `L2..U2` is contained in the type interval `L1..U1` (i.e.
47+
`L1 <: L2` and `U2 <: U1`),
48+
- either `v2` is absent or `v1 = v2`
49+
- `R1 <: R2`
50+
51+
Here we have relied on alpha renaming to bring match the two bound types `X`.
52+
53+
A partially applied type constructor such as `List` is assumed to be equivalent to
54+
its eta expansion. I.e, `List = [+X] => List[X]`. This allows type constructors
55+
to be compared with type lambdas.
56+
57+
## Relationship with Parameterized Type Definitions
58+
59+
A parameterized type definition
60+
```scala
61+
type T[X] = R
62+
```
63+
is regarded as a shorthand for an unparameterized definition with a type lambda as right-hand side:
64+
```scala
65+
type T = [X] => R
66+
```
67+
68+
A parameterized abstract type
69+
```scala
70+
type T[X] >: L <: U
71+
```
72+
is regarded as shorthand for an unparameterized abstract type with type lambdas as bounds.
73+
```scala
74+
type T >: ([X] => L) <: ([X] => U)
75+
```
76+
However, if `L` is `Nothing` it is not parameterized, since `Nothing` is treated as a bottom type for all kinds. For instance,
77+
```scala
78+
type T[-X] <: X => ()
79+
```
80+
is expanded to
81+
```scala
82+
type T >: Nothing <: ([-X] => X => ())
83+
```
84+
instead of
85+
```scala
86+
type T >: ([X] => Nothing) <: ([-X] => X => ())
87+
```
88+
89+
The same expansions apply to type parameters. E.g.
90+
```scala
91+
[F[X] <: Coll[X]]
92+
```
93+
is treated as a shorthand for
94+
```scala
95+
[F >: Nothing <: [X] => Coll[X]]
96+
```
97+
98+
**Note**: The decision to treat `Nothing` as universal bottom type is provisional, and might be changed afer further discussion.
99+
100+
**Note**: Scala 2 and 3 differ in that Scala 2 also treats `Any` as universal top-type. This is not done in Scala 3. See also the discussion on [kind polymorphism](./kind-polymorphism.html)
101+
102+
## Curried Type Parameters
103+
104+
The body of a type lambda can again be a type lambda. Example:
105+
```scala
106+
type TL = [X] => [Y] => (X, Y)
107+
```
108+
Currently, no special provision is made to infer type arguments to such curried type lambdas. This is left for future work.
109+
110+
111+

docs/docs/reference/type-lambdas.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,5 @@ is a shorthand for a plain type definition with a type-lambda as its
2020
right-hand side:
2121

2222
type T = [X] => (X, X)
23+
24+
[More details](./type-lambdas-spec.html)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
object Test extends App {
2+
3+
trait Ord[X]
4+
5+
type TL1 = [X <: Ord[X]] => (X, X)
6+
7+
class C extends Ord[C]
8+
9+
type T1 = TL1[Int] // error: Type argument Int does not conform to upper bound Test.Ord[LazyRef(Int)
10+
type T2 = TL1[C] // OK
11+
12+
class Ref[X](init: X) {
13+
var x: X = init
14+
}
15+
16+
type TL3 = [+X] => Ref[X] // error: covariant type parameter X occurs in nonvariant position in Test.Ref[X]
17+
type TL4[-X] = X => X // error: contravariant type parameter X occurs in covariant position in X => X
18+
19+
def f[F <: [+X] => Any](x: F[String]): F[Any] = x
20+
21+
val sref = new Ref[String]("abc")
22+
val aref: Ref[Any] = f[TL3](sref)
23+
aref.x = 1
24+
val s: String = sref.x
25+
26+
}

tests/neg/type-lambdas.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
object Test extends App {
2+
3+
trait Ord[X]
4+
5+
type TL1 = [X <: Ord[X]] => (X, X) // OK
6+
type TL2 = [X >: Ord[X]] => (X, X) // error: illegal cyclic reference: lower bound Test.Ord[X] of type X refers back to the type itself
7+
8+
}

tests/pos/polytypes.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
object Test {
22

3-
type T = [+X] => (List[X] => List[X])
3+
type T = [X] => (List[X] => List[X])
44

55
def reverse[X](xs: List[X]): List[X] = ???
66

tests/pos/reference/type-lambdas.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,8 @@ object Test {
44

55
type T = [+X, Y] => Map[Y, X]
66

7+
type CTL = [X] => [Y] => (X, Y)
8+
type T3 = CTL[Int][String]
9+
10+
type T2[+X <: X => X] = Any // OK - variance is not checked in param bounds
711
}

tests/pos/seqtype-cycle/Test2.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ package object scala {
33
type Throwable = java.lang.Throwable
44
type IndexOutOfBoundsException = java.lang.IndexOutOfBoundsException
55

6-
type Seq[+A] = scala.collection.Seq[A]
6+
type Seq[A] = scala.collection.Seq[A]
77
val Seq = scala.collection.Seq
88
}

0 commit comments

Comments
 (0)