@@ -55,10 +55,10 @@ typedef adde = (
55
55
@
56
56
Notice, it is a recursive data type. The function we want is
57
57
@felix
58
- fun eval (term: adde) =>
58
+ fun evala (term: adde) =>
59
59
match term with
60
60
| `Int j => j
61
- | `Add (a,b) => eval a + eval b
61
+ | `Add (a,b) => evala a + evala b
62
62
endmatch
63
63
;
64
64
@
@@ -76,7 +76,7 @@ The type variable has eliminated the recursion.
76
76
77
77
And here's the open version of the function:
78
78
@tangle openrec.flx
79
- fun o_eval [T] (eval: T -> int) (term: o_adde[T]) =>
79
+ fun o_evala [T] (eval: T -> int) (term: o_adde[T]) =>
80
80
match term with
81
81
| `Int j => j
82
82
| `Add (a,b) => eval a + eval b
@@ -89,7 +89,7 @@ an extra parameter which is used to eliminate the recursive calls.
89
89
Now we are going to recover the original data type and function:
90
90
@tangle openrec.flx
91
91
typedef adde = o_adde[adde];
92
- fun eval (term:adde):int => o_eval [adde] eval term;
92
+ fun evala (term:adde):int => o_evala [adde] evala term;
93
93
@
94
94
What we have done is close the open data type by
95
95
replacing the type variable with a self-reference.
@@ -100,9 +100,9 @@ of the recursion must be specified.
100
100
101
101
Here is a test case:
102
102
@tangle openrec.flx
103
- var x : adde = `Add (`Add (`Int 39, `Int 2), `Int 1);
104
- var y = eval x ;
105
- println$ y ;
103
+ var xa : adde = `Add (`Add (`Int 39, `Int 2), `Int 1);
104
+ var yaa = evala xa ;
105
+ println$ "ya="+yaa.str ;
106
106
@
107
107
108
108
@h2 Extension to subtraction
@@ -114,18 +114,18 @@ typedef o_sube[T] = (
114
114
| o_adde[T]
115
115
| `Sub of T * T
116
116
);
117
- fun o_eval2 [T] (eval: T -> int) (term: o_sube[T]):int =>
117
+ fun o_evals [T] (eval: T -> int) (term: o_sube[T]):int =>
118
118
match term with
119
119
| `Sub (a,b) => eval a - eval b
120
- | o_adde[T] :>> k => o_eval eval k
120
+ | o_adde[T] :>> k => o_evala eval k
121
121
endmatch
122
122
;
123
123
@
124
124
Notice both the type and function delegate to the existing code for addition
125
125
and only add support for our new operation, subtraction.
126
126
127
127
@h2 Fixpoint operator.
128
- The closure of the the open type and function ic called <em>fixating</em> them.
128
+ The closure of the the open type and function is called <em>fixating</em> them.
129
129
What we have done is to manually introduce the self-reference.
130
130
For the data type, we used a self-refering type alias; for the function
131
131
we used eta-expansion.
@@ -138,7 +138,7 @@ Here's how we do it:
138
138
@tangle openrec.flx
139
139
typefun o_sube_f (T:TYPE) : TYPE => o_sube[T];
140
140
typedef sube = tfix<TYPE> o_sube_f;
141
- fun eval2 (term:sube):int => (fix[sube,int] o_eval2 [sube]) term;
141
+ fun evals (term:sube):int => (fix[sube,int] o_evals [sube]) term;
142
142
@
143
143
144
144
Here are the definitions from the standard library:
@@ -157,11 +157,11 @@ function fixpoint operator.
157
157
Finally a test case:
158
158
159
159
@tangle openrec.flx
160
- var x2 : sube = `Add (`Sub (`Int 39, `Int 2), `Int 1);
161
- var y2 = eval2 x2 ;
162
- var y3 = eval2 x ; // ORGINAL DATA
163
- println$ y2 ;
164
- println$ y3 ;
160
+ var xs : sube = `Add (`Sub (`Int 39, `Int 2), `Int 1);
161
+ var yss = evals xs ;
162
+ var ysa = evals xa ; // ORGINAL DATA
163
+ println$ "ys="+yss.str ;
164
+ println$ "ya="+ysa.str ;
165
165
@
166
166
167
167
It's very important to notice that the original data also works with
@@ -188,20 +188,20 @@ typedef o_mule[T] = (
188
188
| o_adde[T]
189
189
| `Mul of T * T
190
190
);
191
- fun o_eval3 [T] (eval: T -> int) (term: o_mule[T]):int =>
191
+ fun o_evalm [T] (eval: T -> int) (term: o_mule[T]):int =>
192
192
match term with
193
193
| `Mul(a,b) => eval a * eval b
194
- | o_adde[T] :>> k => o_eval eval k
194
+ | o_adde[T] :>> k => o_evala eval k
195
195
endmatch
196
196
;
197
197
typefun o_mule_f (T:TYPE) : TYPE => o_mule[T];
198
198
typedef mule = tfix<TYPE> o_mule_f;
199
- fun eval3 (term:mule):int => (fix[mule,int] o_eval3 [mule]) term;
200
- var x3 : mule = `Add (`Mul(`Int 39, `Int 2), `Int 1);
201
- var y4 = eval3 x3 ;
202
- var y5 = eval3 x ; // ORGINAL DATA
203
- println$ y4 ;
204
- println$ y5 ;
199
+ fun evalm (term:mule):int => (fix[mule,int] o_evalm [mule]) term;
200
+ var xm : mule = `Add (`Mul(`Int 39, `Int 2), `Int 1);
201
+ var ymm = evalm xm ;
202
+ var yma = evalm xa ; // ORGINAL DATA
203
+ println$ "ym="+ymm.str ;
204
+ println$ "ya="+yma.str ;
205
205
@
206
206
207
207
@h2 Mixing it all together
@@ -213,17 +213,17 @@ typedef o_dive[T] = (
213
213
| o_mule[T]
214
214
| `Div of T * T
215
215
);
216
- fun o_eval4 [T] (eval: T -> int) (term: o_dive[T]):int =>
216
+ fun o_evald [T] (eval: T -> int) (term: o_dive[T]):int =>
217
217
match term with
218
218
| `Div(a,b) => eval a / eval b
219
- | o_sube[T] :>> k => o_eval2 eval k
220
- | o_mule[T] :>> k => o_eval3 eval k
219
+ | o_sube[T] :>> k => o_evals eval k
220
+ | o_mule[T] :>> k => o_evalm eval k
221
221
endmatch
222
222
;
223
223
typefun o_dive_f (T:TYPE) : TYPE => o_dive[T];
224
224
typedef dive = tfix<TYPE> o_dive_f;
225
- fun eval4 (term:dive):int => (fix[dive,int] o_eval4 [dive]) term;
226
- var x4 : dive =
225
+ fun evald (term:dive):int => (fix[dive,int] o_evald [dive]) term;
226
+ var xd : dive =
227
227
`Add (
228
228
`Mul (
229
229
`Int 39,
@@ -236,14 +236,79 @@ var x4 : dive =
236
236
`Int 66
237
237
)
238
238
;
239
- var y6 = eval4 x4 ;
240
- var y7 = eval4 x ; // ORGINAL DATA
241
- var y8 = eval4 x2 ; // ORGINAL DATA
242
- var y9 = eval4 x3 ; // ORGINAL DATA
243
- println$ y6 ;
244
- println$ y7 ;
245
- println$ y8 ;
246
- println$ y9 ;
239
+ var ydd = evald xd ;
240
+ var yda = evald xa ; // ORGINAL DATA
241
+ var yds = evald xs ; // ORGINAL DATA
242
+ var ydm = evald xm ; // ORGINAL DATA
243
+ println$ "yd="+ydd.str ;
244
+ println$ "ya="+yda.str ;
245
+ println$ "ys="+yds.str ;
246
+ println$ "ym="+ydm.str ;
247
247
@
248
248
249
+ @h1 Open/Closed Principle
250
+ In "Object Oriented Software Construction" Bertran Meyer named a crucial
251
+ system design principe the "Open/Closed Principle". It states that a system
252
+ should have a unit of modularity which is simultaneously closed so it can
253
+ be used and also open so it can be extended. This gave rise to the idea
254
+ of classes which are closed object factories, but can be extended by
255
+ inheritance.
256
+
257
+ The object oriented version of classes is a catastrophic failure bccause
258
+ of severe restrictions imposed by type systems on the type of arguments of methods,
259
+ which must be contravariant (or invariant), whereas usually we require covariance.
260
+
261
+ Open recursion solves this problem by providing types and function which are
262
+ open to extension, and do not suffer fron the covariance problem because
263
+ type variables are invariant.
264
+
265
+ The system then provides a closure operator, namely the fixpoint operator,
266
+ which locks in covariant behaviour of a particular extension of the base type.
267
+ The types as coproducts, which are covariant, and so base types are subtypes
268
+ of extensions. This impplies the methods of an extension will continue to work
269
+ with data of the base type.
270
+
271
+ @h2 Issues with polymorphic variants
272
+ Polymorphic variants (as in Felix or Ocaml) are essential for open recursion
273
+ to work. However as is the case with all structural types with user supplied
274
+ componenmts, including records, compiler error diagnostics are very hard to
275
+ understand due at least in part the fact that all the components must be listed
276
+ to identify the type. By contrast, nominal types are easy to refer to, since
277
+ they have a single unique and simple name.
278
+
279
+ Ocaml has made significant progress in this area by the diagnostics ability
280
+ to search the symbol to table to see if a compnent set happens to have a named
281
+ alias.
282
+
283
+ @h2 Multivariant Open Recursion
284
+ In our simple example, the open version of a type has a single type variable,
285
+ and the open version of a function has a functional parameter: the type variable
286
+ and the paramater are eliminated by self-reference by using the fixpoint operators.
287
+
288
+ However in complex systems such as a compiler, we usually have main types,
289
+ and auxilliay types they depend on; sometimes, we have two of more major
290
+ types which are convoluted and inseparable: in these cases two or more
291
+ type variables and function parameters may be required
292
+
293
+ This leads to a very difficult combinatorial explosion because now we
294
+ can fixate one type variable whilst leaving the other open. Multiple extensions
295
+ of one type can be used with multiple extensions of another.
296
+
297
+ In Felix, the type system terms in the compiler have at least 6 auxilliary types
298
+ for which multiple flat extensions cold be provided leading to an unmanagably
299
+ large potential set of closed terms. of course this is <em>not</em> a problem
300
+ with open recursion at all: it is a simple fact, and a blessing that open recursion
301
+ provides the machine to represent these choices. The existence of fixpoint combinators
302
+ are then crucial because fixation closures are anonymous and can be provided as required
303
+ "on the fly".
304
+
305
+ Nevertheless, using this technology in complex applications whilst the very environment
306
+ where the modularity provided is of most benefit, is also the environment where
307
+ it is hardest to get right. In the Felix compiler I found it so difficult,
308
+ dynamic typing was prefered; that is, handling terms that should have been
309
+ erased by throwing an exception was a lot easier than constructing a type
310
+ for every use case so as to obtain a compile time error.
311
+
312
+ I think it remains to develop language constructions that can leverage
313
+ this technology in a more manageable way.
249
314
0 commit comments