Skip to content

Commit 8492c21

Browse files
committed
Version 2, picking up on an idea of Adriaan.
1 parent 8a4186f commit 8492c21

File tree

1 file changed

+326
-0
lines changed

1 file changed

+326
-0
lines changed

docs/HigherKinded-v2.md

Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
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

Comments
 (0)