|
| 1 | +Higher-Kinded Types in Dotty V2 |
| 2 | +=============================== |
| 3 | + |
| 4 | +This note outlines how we intend to represent higher-kinded types in |
| 5 | +Dotty. The principal idea is to collapse the four previously |
| 6 | +disparate features of refinements, type parameters, existentials and |
| 7 | +higher-kinded types into just one: refinements of type members. All |
| 8 | +other features will be encoded using these refinements. |
| 9 | + |
| 10 | +The complexity of type systems tends to grow exponentially with the |
| 11 | +number of independent features, because there are an exponential |
| 12 | +number of possible feature interactions. Consequently, a reduction |
| 13 | +from 4 to 1 fundamental features achieves a dramatic reduction of |
| 14 | +complexity. It also adds some nice usablilty improvements, notably in |
| 15 | +the area of partial type application. |
| 16 | + |
| 17 | +This is a second version of the scheme which differs in a key aspect |
| 18 | +from the first one: Following Adriaan's idea, we use traits with type |
| 19 | +members to model type lambdas and type applications. This is both more |
| 20 | +general and more robust than the intersections with type constructor |
| 21 | +traits that we had in the first version. |
| 22 | + |
| 23 | +The duality |
| 24 | +----------- |
| 25 | + |
| 26 | +The core idea: A parameterized class such as |
| 27 | + |
| 28 | + class Map[K, V] |
| 29 | + |
| 30 | +is treated as equivalent to a type with type members: |
| 31 | + |
| 32 | + class Map { type Map$K; type Map$V } |
| 33 | + |
| 34 | +The type members are name-mangled (i.e. `Map$K`) so that they do not conflict with other |
| 35 | +members or parameters named `K` or `V`. |
| 36 | + |
| 37 | +A type-instance such as `Map[String, Int]` would then be treated as equivalent to |
| 38 | + |
| 39 | + Map { type Map$K = String; type Map$V = Int } |
| 40 | + |
| 41 | +Named type parameters |
| 42 | +--------------------- |
| 43 | + |
| 44 | +Type parameters can have unmangled names. This is achieved by adding the `type` keyword |
| 45 | +to a type parameter declaration, analogous to how `val` indicates a named field. For instance, |
| 46 | + |
| 47 | + class Map[type K, type V] |
| 48 | + |
| 49 | +is treated as equivalent to |
| 50 | + |
| 51 | + class Map { type K; type V } |
| 52 | + |
| 53 | +The parameters are made visible as fields. |
| 54 | + |
| 55 | +Wildcards |
| 56 | +--------- |
| 57 | + |
| 58 | +A wildcard type such as `Map[_, Int]` is equivalent to |
| 59 | + |
| 60 | + Map { type Map$V = Int } |
| 61 | + |
| 62 | +I.e. `_`'s omit parameters from being instantiated. Wildcard arguments |
| 63 | +can have bounds. E.g. |
| 64 | + |
| 65 | + Map[_ <: AnyRef, Int] |
| 66 | + |
| 67 | +is equivalent to |
| 68 | + |
| 69 | + Map { type Map$K <: AnyRef; type Map$V = Int } |
| 70 | + |
| 71 | + |
| 72 | +Type parameters in the encodings |
| 73 | +-------------------------------- |
| 74 | + |
| 75 | +The notion of type parameters makes sense even for encoded types, |
| 76 | +which do not contain parameter lists in their syntax. Specifically, |
| 77 | +the type parameters of a type are a sequence of type fields that |
| 78 | +correspond to paraneters in the unencoded type. They are determined as |
| 79 | +follows. |
| 80 | + |
| 81 | + - The type parameters of a class or trait type are those parameter fields declared in the class |
| 82 | + that are not yet instantiated, in the order they are given. Type parameter fields of parents |
| 83 | + are not considered. |
| 84 | + - The type parameters of an abstract type are the type parameters of its upper bound. |
| 85 | + - The type parameters of an alias type are the type parameters of its right hand side. |
| 86 | + - The type parameters of every other type is the empty sequence. |
| 87 | + |
| 88 | +Partial applications |
| 89 | +-------------------- |
| 90 | + |
| 91 | +The definition of type parameters in the previous section leads to a simple model of partial applications. |
| 92 | +Consider for instance: |
| 93 | + |
| 94 | + type Histogram = Map[_, Int] |
| 95 | + |
| 96 | +`Histogram` is a higher-kinded type that still has one type parameter. |
| 97 | +`Histogram[String]` |
| 98 | +would be a possible type instance, and it would be equivalent to `Map[String, Int]`. |
| 99 | + |
| 100 | + |
| 101 | +Modelling polymorphic type declarations |
| 102 | +--------------------------------------- |
| 103 | + |
| 104 | +The partial application scheme gives us a new -- and quite elegant -- |
| 105 | +way to do certain higher-kinded types. But how do we interprete the |
| 106 | +poymorphic types that exist in current Scala? |
| 107 | + |
| 108 | +More concretely, current Scala allows us to write parameterized type |
| 109 | +definitions, abstract types, and type parameters. In the new scheme, |
| 110 | +only classes (and traits) can have parameters and these are treated as |
| 111 | +equivalent to type members. Type aliases and abstract types do not |
| 112 | +allow the definition of parameterized types so we have to interprete |
| 113 | +polymorphic type aliases and abstract types specially. |
| 114 | + |
| 115 | +Modelling polymorphic type aliases: simple case |
| 116 | +----------------------------------------------- |
| 117 | + |
| 118 | +A polymorphic type alias such as |
| 119 | + |
| 120 | + type Pair[T] = Tuple2[T, T] |
| 121 | + |
| 122 | +where `Tuple2` is declared as |
| 123 | + |
| 124 | + class Tuple2[T1, T2] ... |
| 125 | + |
| 126 | +is expanded to a monomorphic type alias like this: |
| 127 | + |
| 128 | + type Pair = Tuple2 { type Tuple2$T2 = Tuple2$T1 } |
| 129 | + |
| 130 | +More generally, each type parameter of the left-hand side must |
| 131 | +appear as a type member of the right hand side type. Type members |
| 132 | +must appear in the same order as their corresponding type parameters. |
| 133 | +References to the type parameter are then translated to references to the |
| 134 | +type member. The type member itself is left uninstantiated. |
| 135 | + |
| 136 | +This technique can expand most polymorphic type aliases appearing |
| 137 | +in Scala codebases but not all of them. For instance, the following |
| 138 | +alias cannot be expanded, because the parameter type `T` is not a |
| 139 | +type member of the right-hand side `List[List[T]]`. |
| 140 | + |
| 141 | + type List2[T] = List[List[T]] |
| 142 | + |
| 143 | +We scanned the Scala standard library for occurrences of polymorphic |
| 144 | +type aliases and determined that only two occurrences could not be expanded. |
| 145 | +In `io/Codec.scala`: |
| 146 | + |
| 147 | + type Configure[T] = (T => T, Boolean) |
| 148 | + |
| 149 | +And in `collection/immutable/HashMap.scala`: |
| 150 | + |
| 151 | + private type MergeFunction[A1, B1] = ((A1, B1), (A1, B1)) => (A1, B1) |
| 152 | + |
| 153 | +For these cases, we use a fall-back scheme that models a parameterized alias as a |
| 154 | +`Lambda` type. |
| 155 | + |
| 156 | +Modelling polymorphic type aliases: general case |
| 157 | +------------------------------------------------ |
| 158 | + |
| 159 | +A polymorphic type alias such as |
| 160 | + |
| 161 | + type List2D[T] = List[List[T]] |
| 162 | + |
| 163 | +is represented as a monomorphic type alias of a type lambda. Here's the expanded version of |
| 164 | +the definition above: |
| 165 | + |
| 166 | + type List2D = Lambda$I { type Apply = List[List[$hkArg$0]] } |
| 167 | + |
| 168 | +Here, `Lambda$I` is a standard trait defined as follows: |
| 169 | + |
| 170 | + trait Lambda$I[type $hkArg$0] { type +Apply } |
| 171 | + |
| 172 | +The `I` suffix of the `Lambda` trait indicates that it has one invariant type parameter (named $hkArg$0). |
| 173 | +Other suffixes are `P` for covariant type parameters, and `N` for contravariant type parameters. Lambda traits can |
| 174 | +have more than one type parameter. For instance, here is a trait with contravariant and covariant type parameters: |
| 175 | + |
| 176 | + trait Lambda$NP[type -$hkArg$0, +$hkArg1] { type +Apply } extends Lambda$IP with Lambda$PI |
| 177 | + |
| 178 | +Aside: the `+` prefix in front of `Apply` indicates that `Apply` is a covariant type field. Dotty |
| 179 | +admits variance annotations on type members). |
| 180 | + |
| 181 | +The definition of `Lambda$NP` shows that `Lambda` traits form a subtyping hierarchy: Traits which |
| 182 | +have covariant or contravariant type parameters are subtypes of traits which don't. The supertraits |
| 183 | +of `Lambda$NP` would themselves be written as follows. |
| 184 | + |
| 185 | + trait Lambda$IP[type $hkArg$0, +$hkArg1] { type +Apply } extends Lambda$II |
| 186 | + trait Lambda$NI[type -$hkArg$0, $hkArg1] { type +Apply } extends Lambda$II |
| 187 | + trait Lambda$II[type -hkArg$0, $hkArg1] { type +Apply } |
| 188 | + |
| 189 | +`Lambda` traits are special in that |
| 190 | +they influence how type applications are expanded: If the standard type application `T[X1, ..., Xn]` |
| 191 | +leads to a subtype `S` of a type instance |
| 192 | + |
| 193 | + LambdaXYZ { type Arg1 = T1; ...; type ArgN = Tn; type Apply ... } |
| 194 | + |
| 195 | +where all argument fields `Arg1, ..., ArgN` are concretely defined |
| 196 | +and the definition of the `Apply` field may be either abstract or concrete, then the application |
| 197 | +is further expanded to `S # Apply`. |
| 198 | + |
| 199 | +For instance, the type instance `List2D[String]` would be expanded to |
| 200 | + |
| 201 | + Lambda$I { type $hkArg$0 = String; type Apply = List[List[String]] } # Apply |
| 202 | + |
| 203 | +which in turn simplifies to `List[List[String]]`. |
| 204 | + |
| 205 | +2nd Example: Consider the two aliases |
| 206 | + |
| 207 | + type RMap[K, V] = Map[V, K]] |
| 208 | + type RRMap[K, V] = RMap[V, K] |
| 209 | + |
| 210 | +These expand as follows: |
| 211 | + |
| 212 | + type RMap = Lambda$II { self1 => type Apply = Map[self1.$hkArg$1, self1.$hkArg$0] } |
| 213 | + type RRMap = Lambda$II { self2 => type Apply = RMap[self2.$hkArg$1, self2.$hkArg$0] } |
| 214 | + |
| 215 | +Substituting the definition of `RMap` and expanding the type application gives: |
| 216 | + |
| 217 | + type RRMap = Lambda$II { self2 => type Apply = |
| 218 | + Lambda$II { self1 => type Apply = Map[self1.$hkArg$1, self1.$hkArg$0] } |
| 219 | + { type $hkArg$0 = self2.$hkArg$1; type $hkArg$1 = self2.$hkArg$0 } # Apply } |
| 220 | + |
| 221 | +Substituting the definitions for `self1.$hkArg${1,2}` gives: |
| 222 | + |
| 223 | + type RRMap = Lambda$II { self2 => type Apply = |
| 224 | + Lambda$II { self1 => type Apply = Map[self2.$hkArg$0, self2.$hkArg$1] } |
| 225 | + { type $hkArg$0 = self2.$hkArg$1; type $hkArg$1 = self2.$hkArg$0 } # Apply } |
| 226 | + |
| 227 | +Simplifiying the `# Apply` selection gives: |
| 228 | + |
| 229 | + type RRMap = Lambda$II { self2 => type Apply = Map[self2.$hkArg$0, self2.$hkArg$1] } |
| 230 | + |
| 231 | +This can be regarded as the eta-expanded version of `Map`. It has the same expansion as |
| 232 | + |
| 233 | + type IMap[K, V] = Map[K, V] |
| 234 | + |
| 235 | + |
| 236 | +Modelling higher-kinded types |
| 237 | +----------------------------- |
| 238 | + |
| 239 | +The encoding of higher-kinded types uses again the `Lambda` traits to represent type constructors. |
| 240 | +Consider the higher-kinded type declaration |
| 241 | + |
| 242 | + type Rep[T] |
| 243 | + |
| 244 | +We expand this to |
| 245 | + |
| 246 | + type Rep <: Lambda$I |
| 247 | + |
| 248 | +The type parameters of `Rep` are the type parameters of its upper bound, so |
| 249 | +`Rep` is a unary type constructor. |
| 250 | + |
| 251 | +More generally, a higher-kinded type declaration |
| 252 | + |
| 253 | + type T[v1 X1 >: S1 <: U1, ..., vn XN >: S1 <: UN] >: SR <: UR |
| 254 | + |
| 255 | +is encoded as |
| 256 | + |
| 257 | + type T <: LambdaV1...Vn { self => |
| 258 | + type v1 $hkArg$0 >: s(S1) <: s(U1) |
| 259 | + ... |
| 260 | + type vn $hkArg$N >: s(SN) <: s(UN) |
| 261 | + type Apply >: s(SR) <: s(UR) |
| 262 | + } |
| 263 | + |
| 264 | +where `s` is the substitution `[XI := self.$hkArg$I | I = 1,...,N]`. |
| 265 | + |
| 266 | +If we instantiate `Rep` with a type argument, this is expanded as was explained before. |
| 267 | + |
| 268 | + Rep[String] |
| 269 | + |
| 270 | +would expand to |
| 271 | + |
| 272 | + Rep { type $hkArg$0 = String } # Apply |
| 273 | + |
| 274 | +If we instantiate the higher-kinded type with a concrete type constructor (i.e. a parameterized |
| 275 | +trait or class), we have to do one extra adaptation to make it work. The parameterized trait |
| 276 | +or class has to be eta-expanded so that it comforms to the `Lambda` bound. For instance, |
| 277 | + |
| 278 | + type Rep = Set |
| 279 | + |
| 280 | +would expand to |
| 281 | + |
| 282 | + type Rep = Lambda1 { type Apply = Set[$hkArg$0] } |
| 283 | + |
| 284 | +Or, |
| 285 | + |
| 286 | + type Rep = Map[String, _] |
| 287 | + |
| 288 | +would expand to |
| 289 | + |
| 290 | + type Rep = Lambda1 { type Apply = Map[String, $hkArg$0] } |
| 291 | + |
| 292 | + |
| 293 | +Full example |
| 294 | +------------ |
| 295 | + |
| 296 | +Consider the higher-kinded `Functor` type class |
| 297 | + |
| 298 | + class Functor[F[_]] { |
| 299 | + def map[A, B](f: A => B): F[A] => F[B] |
| 300 | + } |
| 301 | + |
| 302 | +This would be represented as follows: |
| 303 | + |
| 304 | + class Functor[F <: Lambda1] { |
| 305 | + def map[A, B](f: A => B): F { type $hkArg$0 = A } # Apply => F { type $hkArg$0 = B } # Apply |
| 306 | + } |
| 307 | + |
| 308 | +The type `Functor[List]` would be represented as follows |
| 309 | + |
| 310 | + Functor { |
| 311 | + type F = Lambda1 { type Apply = List[$hkArg$0] } |
| 312 | + } |
| 313 | + |
| 314 | +Now, assume we have a value |
| 315 | + |
| 316 | + val ml: Functor[List] |
| 317 | + |
| 318 | +Then `ml.map` would have type |
| 319 | + |
| 320 | + s(F { type $hkArg$0 = A } # Apply => F { type $hkArg$0 = B } # Apply) |
| 321 | + |
| 322 | +where `s` is the substitution of `[F := Lambda1 { type Apply = List[$hkArg$0] }]`. |
| 323 | +This gives: |
| 324 | + |
| 325 | + Lambda1 { type Apply = List[$hkArg$0] } { type $hkArg$0 = A } # Apply |
| 326 | + => Lambda1 { type Apply = List[$hkArg$0] } { type $hkArg$0 = B } # Apply |
| 327 | + |
| 328 | +This type simplifies to: |
| 329 | + |
| 330 | + List[A] => List[B] |
| 331 | + |
| 332 | +Status of # |
| 333 | +----------- |
| 334 | + |
| 335 | +In the scheme above we have silently assumed that `#` "does the right |
| 336 | +thing", i.e. that the types are well-formed and we can collapse a type |
| 337 | +alias with a `#` projection, thereby giving us a form of beta |
| 338 | +reduction. |
| 339 | + |
| 340 | +In Scala 2.x, this would not work, because `T#X` means `x.X forSome { val x: T }`. |
| 341 | +Hence, two occurrences of `Rep[Int]` say, would not be recognized to be equal because the |
| 342 | +existential would be opened each time afresh. |
| 343 | + |
| 344 | +In pre-existentials Scala, this would not have worked either. There, `T#X` was a fundamental |
| 345 | +type constructor, but was restricted to alias types or classes for both `T` and `X`. |
| 346 | +Roughly, `#` was meant to encode Java's inner classes. In Java, given the classes |
| 347 | + |
| 348 | + class Outer { class Inner } |
| 349 | + class Sub1 extends Outer |
| 350 | + class Sub2 extends Outer |
| 351 | + |
| 352 | +The types `Outer#Inner`, `Sub1#Inner` and `Sub2#Inner` would all exist and be |
| 353 | +regarded as equal to each other. But if `Outer` had abstract type members this would |
| 354 | +not work, since an abstract type member could be instantiated differently in `Sub1` and `Sub2`. |
| 355 | +Assuming that `Sub1#Inner = Sub2#Inner` could then lead to a soundness hole. To avoid soundness |
| 356 | +problems, the types in `X#Y` were restricted so that `Y` was (an alias of) a class type and |
| 357 | +`X` was (an alias of) a class type with no abstract type members. |
| 358 | + |
| 359 | +I believe we can go back to regarding `T#X` as a fundamental type constructor, the way it |
| 360 | +was done in pre-existential Scala, but with the following relaxed restriction: |
| 361 | + |
| 362 | + _In a type selection `T#x`, `T` is not allowed to have any abstract members different from `X`._ |
| 363 | + |
| 364 | +This would typecheck the higher-kinded types examples, because they only project with `# Apply` once all |
| 365 | +`$hkArg$` type members are fully instantiated. |
| 366 | + |
| 367 | +It would be good to study this rule formally, trying to verify its soundness. |
| 368 | + |
| 369 | + |
| 370 | + |
| 371 | + |
| 372 | + |
| 373 | + |
| 374 | + |
| 375 | + |
0 commit comments