|
| 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 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 parameterize 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 type members so we have to interprete |
| 113 | +polymorphic type aliases and abstract types specially. |
| 114 | + |
| 115 | +Modelling polymorphic type aliases |
| 116 | +---------------------------------- |
| 117 | + |
| 118 | +A polymorphic type alias such as |
| 119 | + |
| 120 | + type Pair[T] = (T, T) |
| 121 | + |
| 122 | +is represented as a monomorphic type alias of a type lambda. Here's the expanded version of |
| 123 | +the definition above: |
| 124 | + |
| 125 | + type Pair = Lambda1 { type Apply = (Arg1, Arg1) } |
| 126 | + |
| 127 | +Here, `Lambda1` is a standard trait defined as follows: |
| 128 | + |
| 129 | + trait Lambda1[type Arg1, type Apply] |
| 130 | + |
| 131 | +According to our definitions of type parameters `Lambda1` has two type parameters |
| 132 | +and `Pair` has one. |
| 133 | + |
| 134 | +There are `LambdaN` traits for higher arities as well. `Lambda` traits are special in that |
| 135 | +they influence how type applications are expanded: If standard type applicatuon `T[X1, ..., Xn]` |
| 136 | +leads to a subtype `S` of a type instance |
| 137 | + |
| 138 | + LambdaN { type Arg1 = T1; ...; type ArgN = Tn; type Apply ... } |
| 139 | + |
| 140 | +where all argument fields `Arg1, ..., ArgN` are concretely defined |
| 141 | +and the definition of the `Apply` field may be either abstract or concrete, then the application |
| 142 | +is further expanded to `S # Apply`. |
| 143 | + |
| 144 | +For instance, the type instance `Pair[String]` would be expanded to |
| 145 | + |
| 146 | + Lambda1 { type Arg1 = String; type Apply = (Arg1, Arg1) } # Apply |
| 147 | + |
| 148 | +which turns out to be equal to `(String, String)`. |
| 149 | + |
| 150 | +2nd Example: Consider the two aliases |
| 151 | + |
| 152 | + type RMap[K, V] = Map[V, K]] |
| 153 | + type RRMap[K, V] = RMap[V, K] |
| 154 | + |
| 155 | +These expand as follows: |
| 156 | + |
| 157 | + type RMap = Lambda2 { self1 => type Apply = Map[self1.Arg2, self1.Arg1] } |
| 158 | + type RRMap = Lambda2 { self2 => type Apply = RMap[self2.Arg2, self2.Arg1] } |
| 159 | + |
| 160 | +Substituting the definition of `RMap` and expanding the type application gives: |
| 161 | + |
| 162 | + type RRMap = Lambda2 { self2 => type Apply = |
| 163 | + Lambda2 { self1 => type Apply = Map[self1.Arg2, self1.Arg1] } |
| 164 | + { type Arg1 = self2.Arg2; type Arg2 = self2.Arg1 } # Apply } |
| 165 | + |
| 166 | +Substituting the definitions `self1.Arg{1,2}` gives: |
| 167 | + |
| 168 | + type RRMap = Lambda2 { self2 => type Apply = |
| 169 | + Lambda2 { self1 => type Apply = Map[self2.Arg1, self2.Arg2] } |
| 170 | + { type Arg1 = self2.Arg2; type Arg2 = self2.Arg1 } # Apply } |
| 171 | + |
| 172 | +Simplifiying the `# Apply` selection gives: |
| 173 | + |
| 174 | + type RRMap = Lambda2 { self2 => type Apply = Map[self2.Arg1, self2.Arg2] } |
| 175 | + |
| 176 | +This can be regarded as the eta-expanded version of `Map`. It has the same expansion as |
| 177 | + |
| 178 | + type IMap[K, V] = Map[K, V] |
| 179 | + |
| 180 | + |
| 181 | +Modelling higher-kinded types |
| 182 | +----------------------------- |
| 183 | + |
| 184 | +The encoding of higher-kinded types uses again the `Lambda` traits to represent type constructors. |
| 185 | +Consider the higher-kinded type declaration |
| 186 | + |
| 187 | + type Rep[T] |
| 188 | + |
| 189 | +We expand this to |
| 190 | + |
| 191 | + type Rep <: Lambda1 |
| 192 | + |
| 193 | +The type parameters of `Rep` are the type parameters of its upper bound, so |
| 194 | +`Rep` is a uniary type constructor. |
| 195 | + |
| 196 | +More generally, a higher-kinded type declaration |
| 197 | + |
| 198 | + type T[X1 >: S1 <: U1, ..., XN >: S1 <: UN] >: SR <: UR |
| 199 | + |
| 200 | +is encoded as |
| 201 | + |
| 202 | + type T <: LambdaN { self => |
| 203 | + type Arg1 >: s(S1) <: s(U1) |
| 204 | + ... |
| 205 | + type ArgN >: s(SN) <: s(UN) |
| 206 | + type Apply >: s(SR) <: s(UR) |
| 207 | + } |
| 208 | + |
| 209 | +where `s` is the substitution `[XI := self.ArgI | I = 1,...,N]`. |
| 210 | + |
| 211 | +If we instantiate `Rep` with a type argument, this is expanded as was explained before. |
| 212 | + |
| 213 | + Rep[String] |
| 214 | + |
| 215 | +would expand to |
| 216 | + |
| 217 | + Rep { type Arg1 = String } # Apply |
| 218 | + |
| 219 | +If we instantiate the higher-kinded type with a concrete type constructor (i.e. a parameterized |
| 220 | +trait or class, we have to do one extra adaptation to make it work. The parameterized trait |
| 221 | +or class has to be eta-expansed so that it comforms to the `Lambda` bound. For instance, |
| 222 | + |
| 223 | + type Rep = Set |
| 224 | + |
| 225 | +would expand to |
| 226 | + |
| 227 | + type Rep = Lambda1 { type Apply = Set[Arg1] } |
| 228 | + |
| 229 | +Or, |
| 230 | + |
| 231 | + type Rep = Map[String, _] |
| 232 | + |
| 233 | +would expand to |
| 234 | + |
| 235 | + type Rep = Lambda1 { type Apply = Map[String, Arg1] } |
| 236 | + |
| 237 | + |
| 238 | +Full example |
| 239 | +------------ |
| 240 | + |
| 241 | +Consider the higher-kinded `Functor` type class |
| 242 | + |
| 243 | + class Functor[F[_]] { |
| 244 | + def map[A, B](f: A => B): F[A] => F[B] |
| 245 | + } |
| 246 | + |
| 247 | +This would be represented as follows: |
| 248 | + |
| 249 | + class Functor[F <: Lambda1] { |
| 250 | + def map[A, B](f: A => B): F { type Arg1 = A } # Apply => F { type Arg1 = B } # Apply |
| 251 | + } |
| 252 | + |
| 253 | +The type `Functor[List]` would be represented as follows |
| 254 | + |
| 255 | + Functor { |
| 256 | + type F = Lambda1 { type Apply = List[Arg1] } |
| 257 | + } |
| 258 | + |
| 259 | +Now, assume we have a value |
| 260 | + |
| 261 | + val ml: Functor[List] |
| 262 | + |
| 263 | +Then `ml.map` would have type |
| 264 | + |
| 265 | + s(F { type Arg1 = A } # Apply => F { type Arg1 = B } # Apply) |
| 266 | + |
| 267 | +where `s` is the substitution of `[F := Lambda1 { type Apply = List[Arg1] }]`. |
| 268 | +This gives: |
| 269 | + |
| 270 | + Lambda1 { type Apply = List[Arg1] } { type Arg1 = A } # Apply |
| 271 | + => Lambda1 { type Apply = List[Arg1] } { type Arg1 = B } # Apply |
| 272 | + |
| 273 | +This type simplifies to: |
| 274 | + |
| 275 | + List[A] => List[B] |
| 276 | + |
| 277 | +Status of # |
| 278 | +----------- |
| 279 | + |
| 280 | +In the scheme above we have silently assumed that `#` "does the right |
| 281 | +thing", i.e. that the types are well-formed and we can collapse a type |
| 282 | +alias with a `#` projection, thereby giving us a form of beta |
| 283 | +reduction. |
| 284 | + |
| 285 | +In Scala 2.x, this would not work, because `T#X` means `x.X forSome { val x: T }`. |
| 286 | +Hence, two occurrences of `Rep[Int]` say, would not be recognized to be equal because the |
| 287 | +existential would be opened each time afresh. |
| 288 | + |
| 289 | +In pre-existentials Scala, this would not have worked either. There, `T#X` was a fundamental |
| 290 | +type constructor, but was restricted to alias types or classes for both `T` and `X`. |
| 291 | +Roughly, `#` was meant to encode Java's inner classes. In Java, given the classes |
| 292 | + |
| 293 | + class Outer { class Inner } |
| 294 | + class Sub1 extends Outer |
| 295 | + class Sub2 extends Outer |
| 296 | + |
| 297 | +The types `Outer#Inner`, `Sub1#Inner` and `Sub2#Inner` would all exist and be |
| 298 | +regarded as equal to each other. But if `Outer` had abstract type members this would |
| 299 | +not work, since that type member could be instantiated differently in `Sub1` and `Sub2`. |
| 300 | +Assuming that `Sub1#Inner = Sub2#Inner` could then lead to a soundness hole. To avoid soundness |
| 301 | +problems, the types in `X#Y` were restricted so that `Y` was (an alias of) a class type and |
| 302 | +`X` was (an alias of) a class type with no abstract type members. |
| 303 | + |
| 304 | +I believe we can drop this restriction and allow arbitrary type projects `X#Y` if we |
| 305 | +are more careful with the subtyping rules. Specifically: |
| 306 | + |
| 307 | + A # X <: B # X |
| 308 | + |
| 309 | +if either `A = B`, or the following three conditions hold: |
| 310 | + |
| 311 | + 1. `X` is (an alias of) a class type, |
| 312 | + 2. `B` is (an alias of) a class type without abstract type members. |
| 313 | + 3. `A <: B`. |
| 314 | + |
| 315 | +In essence, we allow abstract types `X`, `Y` in a project `X#Y` but we prevent in this |
| 316 | +case hiding conflicting type information in a subtype widening. |
| 317 | + |
| 318 | +It would be good to study these rules formally, trying to verify their soundness. |
| 319 | + |
| 320 | + |
| 321 | + |
| 322 | + |
| 323 | + |
| 324 | + |
| 325 | + |
| 326 | + |
0 commit comments