aboutsummaryrefslogtreecommitdiff
path: root/docs/HigherKinded-v2.md
blob: fffe357a5ffc0bf756024534d255b2d62bb66653 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
Higher-Kinded Types in Dotty V2
===============================

This note outlines how we intend to represent higher-kinded types in
Dotty.  The principal idea is to collapse the four previously
disparate features of refinements, type parameters, existentials and
higher-kinded types into just one: refinements of type members. All
other features will be encoded using these refinements.

The complexity of type systems tends to grow exponentially with the
number of independent features, because there are an exponential
number of possible feature interactions. Consequently, a reduction
from 4 to 1 fundamental features achieves a dramatic reduction of
complexity. It also adds some nice usablilty improvements, notably in
the area of partial type application.

This is a second version of the scheme which differs in a key aspect
from the first one: Following Adriaan's idea, we use traits with type
members to model type lambdas and type applications. This is both more
general and more robust than the intersections with type constructor
traits that we had in the first version.

The duality
-----------

The core idea: A parameterized class such as

    class Map[K, V]

is treated as equivalent to a type with type members:

    class Map { type Map$K; type Map$V }

The type members are name-mangled (i.e. `Map$K`) so that they do not conflict with other
members or parameters named `K` or `V`.

A type-instance such as `Map[String, Int]` would then be treated as equivalent to

    Map { type Map$K = String; type Map$V = Int }

Named type parameters
---------------------

Type parameters can have unmangled names. This is achieved by adding the `type` keyword
to a type parameter declaration, analogous to how `val` indicates a named field. For instance,

    class Map[type K, type V]

is treated as equivalent to

    class Map { type K; type V }

The parameters are made visible as fields.

Wildcards
---------

A wildcard type such as `Map[_, Int]` is equivalent to

    Map { type Map$V = Int }

I.e. `_`'s omit parameters from being instantiated. Wildcard arguments
can have bounds. E.g.

    Map[_ <: AnyRef, Int]

is equivalent to

    Map { type Map$K <: AnyRef; type Map$V = Int }


Type parameters in the encodings
--------------------------------

The notion of type parameters makes sense even for encoded types,
which do not contain parameter lists in their syntax. Specifically,
the type parameters of a type are a sequence of type fields that
correspond to parameters in the unencoded type. They are determined as
follows.

 - The type parameters of a class or trait type are those parameter fields declared in the class
   that are not yet instantiated, in the order they are given. Type parameter fields of parents
   are not considered.
 - The type parameters of an abstract type are the type parameters of its upper bound.
 - The type parameters of an alias type are the type parameters of its right hand side.
 - The type parameters of every other type is the empty sequence.

Partial applications
--------------------

The definition of type parameters in the previous section leads to a simple model of partial applications.
Consider for instance:

    type Histogram = Map[_, Int]

`Histogram` is a higher-kinded type that still has one type parameter.
`Histogram[String]`
would be a possible type instance, and it would be equivalent to `Map[String, Int]`.


Modelling polymorphic type declarations
---------------------------------------

The partial application scheme gives us a new -- and quite elegant --
way to do certain higher-kinded types. But how do we interprete the
poymorphic types that exist in current Scala?

More concretely, current Scala allows us to write parameterized type
definitions, abstract types, and type parameters. In the new scheme,
only classes (and traits) can have parameters and these are treated as
equivalent to type members. Type aliases and abstract types do not
allow the definition of parameterized types so we have to interprete
polymorphic type aliases and abstract types specially.

Modelling polymorphic type aliases: simple case
-----------------------------------------------

A polymorphic type alias such as

    type Pair[T] = Tuple2[T, T]

where `Tuple2` is declared as

    class Tuple2[T1, T2] ...

is expanded to a monomorphic type alias like this:

    type Pair = Tuple2 { type Tuple2$T2 = Tuple2$T1 }

More generally, each type parameter of the left-hand side must
appear as a type member of the right hand side type. Type members
must appear in the same order as their corresponding type parameters.
References to the type parameter are then translated to references to the
type member. The type member itself is left uninstantiated.

This technique can expand most polymorphic type aliases appearing
in Scala codebases but not all of them. For instance, the following
alias cannot be expanded, because the parameter type `T` is not a
type member of the right-hand side `List[List[T]]`.

    type List2[T] = List[List[T]]

We scanned the Scala standard library for occurrences of polymorphic
type aliases and determined that only two occurrences could not be expanded.
In `io/Codec.scala`:

    type Configure[T] = (T => T, Boolean)

And in `collection/immutable/HashMap.scala`:

    private type MergeFunction[A1, B1] = ((A1, B1), (A1, B1)) => (A1, B1)

For these cases, we use a fall-back scheme that models a parameterized alias as a
`Lambda` type.

Modelling polymorphic type aliases: general case
------------------------------------------------

A polymorphic type alias such as

    type List2D[T] = List[List[T]]

is represented as a monomorphic type alias of a type lambda. Here's the expanded version of
the definition above:

    type List2D = Lambda$I { type Apply = List[List[$hkArg$0]] }

Here, `Lambda$I` is a standard trait defined as follows:

    trait Lambda$I[type $hkArg$0] { type +Apply }

The `I` suffix of the `Lambda` trait indicates that it has one invariant type parameter (named $hkArg$0).
Other suffixes are `P` for covariant type parameters, and `N` for contravariant type parameters. Lambda traits can
have more than one type parameter. For instance, here is a trait with contravariant and covariant type parameters:

    trait Lambda$NP[type -$hkArg$0, +$hkArg1] { type +Apply } extends Lambda$IP with Lambda$PI

Aside: the `+` prefix in front of `Apply` indicates that `Apply` is a covariant type field. Dotty
admits variance annotations on type members.

The definition of `Lambda$NP` shows that `Lambda` traits form a subtyping hierarchy: Traits which
have covariant or contravariant type parameters are subtypes of traits which don't. The supertraits
of `Lambda$NP` would themselves be written as follows.

    trait Lambda$IP[type $hkArg$0, +$hkArg1] { type +Apply } extends Lambda$II
    trait Lambda$NI[type -$hkArg$0, $hkArg1] { type +Apply } extends Lambda$II
    trait Lambda$II[type -hkArg$0, $hkArg1] { type +Apply }

`Lambda` traits are special in that
they influence how type applications are expanded: If the standard type application `T[X1, ..., Xn]`
leads to a subtype `S` of a type instance

    LambdaXYZ { type Arg1 = T1; ...; type ArgN = Tn; type Apply ... }

where all argument fields `Arg1, ..., ArgN` are concretely defined
and the definition of the `Apply` field may be either abstract or concrete, then the application
is further expanded to `S # Apply`.

For instance, the type instance `List2D[String]` would be expanded to

    Lambda$I { type $hkArg$0 = String; type Apply = List[List[String]] } # Apply

which in turn simplifies to `List[List[String]]`.

2nd Example: Consider the two aliases

    type RMap[K, V] = Map[V, K]]
    type RRMap[K, V] = RMap[V, K]

These expand as follows:

    type RMap  = Lambda$II { self1 => type Apply = Map[self1.$hkArg$1, self1.$hkArg$0] }
    type RRMap = Lambda$II { self2 => type Apply = RMap[self2.$hkArg$1, self2.$hkArg$0] }

Substituting the definition of `RMap` and expanding the type application gives:

    type RRMap = Lambda$II { self2 => type Apply =
                   Lambda$II { self1 => type Apply = Map[self1.$hkArg$1, self1.$hkArg$0] }
                     { type $hkArg$0 = self2.$hkArg$1; type $hkArg$1 = self2.$hkArg$0 } # Apply }

Substituting the definitions for `self1.$hkArg${1,2}` gives:

    type RRMap = Lambda$II { self2 => type Apply =
                   Lambda$II { self1 => type Apply = Map[self2.$hkArg$0, self2.$hkArg$1] }
                      { type $hkArg$0 = self2.$hkArg$1; type $hkArg$1 = self2.$hkArg$0 } # Apply }

Simplifiying the `# Apply` selection gives:

    type RRMap = Lambda$II { self2 => type Apply = Map[self2.$hkArg$0, self2.$hkArg$1] }

This can be regarded as the eta-expanded version of `Map`. It has the same expansion as

    type IMap[K, V] = Map[K, V]


Modelling higher-kinded types
-----------------------------

The encoding of higher-kinded types uses again the `Lambda` traits to represent type constructors.
Consider the higher-kinded type declaration

    type Rep[T]

We expand this to

    type Rep <: Lambda$I

The type parameters of `Rep` are the type parameters of its upper bound, so
`Rep` is a unary type constructor.

More generally, a higher-kinded type declaration

     type T[v1 X1 >: S1 <: U1, ..., vn XN >: S1 <: UN] >: SR <: UR

is encoded as

     type T <: LambdaV1...Vn { self =>
       type v1 $hkArg$0 >: s(S1) <: s(U1)
       ...
       type vn $hkArg$N >: s(SN) <: s(UN)
       type Apply >: s(SR) <: s(UR)
     }

where `s` is the substitution `[XI := self.$hkArg$I | I = 1,...,N]`.

If we instantiate `Rep` with a type argument, this is expanded as was explained before.

    Rep[String]

would expand to

    Rep { type $hkArg$0 = String } # Apply

If we instantiate the higher-kinded type with a concrete type constructor (i.e. a parameterized
trait or class), we have to do one extra adaptation to make it work. The parameterized trait
or class has to be eta-expanded so that it comforms to the `Lambda` bound. For instance,

    type Rep = Set

would expand to

    type Rep = Lambda1 { type Apply = Set[$hkArg$0] }

Or,

    type Rep = Map[String, _]

would expand to

    type Rep = Lambda1 { type Apply = Map[String, $hkArg$0] }


Full example
------------

Consider the higher-kinded `Functor` type class

    class Functor[F[_]] {
       def map[A, B](f: A => B): F[A] => F[B]
    }

This would be represented as follows:

    class Functor[F <: Lambda1] {
       def map[A, B](f: A => B): F { type $hkArg$0 = A } # Apply  =>  F { type $hkArg$0 = B } # Apply
    }

The type `Functor[List]` would be represented as follows

    Functor {
       type F = Lambda1 { type Apply = List[$hkArg$0] }
    }

Now, assume we have a value

    val ml: Functor[List]

Then `ml.map` would have type

    s(F { type $hkArg$0 = A } # Apply  =>  F { type $hkArg$0 = B } # Apply)

where `s` is the substitution of `[F := Lambda1 { type Apply = List[$hkArg$0] }]`.
This gives:

    Lambda1 { type Apply = List[$hkArg$0] } { type $hkArg$0 = A } # Apply
     =>  Lambda1 { type Apply = List[$hkArg$0] } { type $hkArg$0 = B } # Apply

This type simplifies to:

     List[A] => List[B]

Status of #
-----------

In the scheme above we have silently assumed that `#` "does the right
thing", i.e. that the types are well-formed and we can collapse a type
alias with a `#` projection, thereby giving us a form of beta
reduction.

In Scala 2.x, this would not work, because `T#X` means `x.X forSome { val x: T }`.
Hence, two occurrences of `Rep[Int]` say, would not be recognized to be equal because the
existential would be opened each time afresh.

In pre-existentials Scala, this would not have worked either. There, `T#X` was a fundamental
type constructor, but was restricted to alias types or classes for both `T` and `X`.
Roughly, `#` was meant to encode Java's inner classes. In Java, given the classes

    class Outer { class Inner }
    class Sub1 extends Outer
    class Sub2 extends Outer

The types `Outer#Inner`, `Sub1#Inner` and `Sub2#Inner` would all exist and be
regarded as equal to each other. But if `Outer` had abstract type members this would
not work, since an abstract type member could be instantiated differently in `Sub1` and `Sub2`.
Assuming that `Sub1#Inner = Sub2#Inner` could then lead to a soundness hole. To avoid soundness
problems, the types in `X#Y` were restricted so that `Y` was (an alias of) a class type and
`X` was (an alias of) a class type with no abstract type members.

I believe we can go back to regarding `T#X` as a fundamental type constructor, the way it
was done in pre-existential Scala, but with the following relaxed restriction:

    _In a type selection `T#x`, `T` is not allowed to have any abstract members different from `X`._

This would typecheck the higher-kinded types examples, because they only project with `# Apply` once all
`$hkArg$` type members are fully instantiated.

It would be good to study this rule formally, trying to verify its soundness.