git.haldean.org ubik / 64fc6ea
start writing about recursive closures Haldean Brown 5 years ago
1 changed file(s) with 39 addition(s) and 87 deletion(s). Raw diff Collapse all Expand all
11 Haldean Brown First draft: Nov 2016
22 Last updated: Nov 2016
33
4 Status: Complete
4 Status: In progress
55
66 ------------------------------------------------------------------------
77
4747 ! t 1
4848 }
4949
50 In this example, the closure transformation should take t and turn it
51 into this:
50 In this example, t needs access to its own value from within its definition.
51 However, since t is a local binding, it's never actually assigned to a name
52 that persists past its own scope. To handle this, we transform t to take itself
53 as a parameter; callers then call t by passing it itself. The resulting
54 function then looks like:
5255
53 : t = (\t x -> ? {
56 : t = \$t x -> ? {
5457 . eq x 0 => "ok\n"
55 . => t 0
56 }) t
57
58 But now we find ourselves referencing t before we've defined it, and we
59 can quickly get into a circular reference. Here, we transform t into a
60 top-level anonymous function with its own value:
61
62 : anonymous-t =
63 \t x -> ? {
64 . eq x 0 => "ok\n"
65 . => t 0
58 . => $t $t 0
6659 }
6760
68 ! {
69 : t = anonymous-t (anonymous-t anonymous-t)
70 ! t 1
71 }
61 This poses a problem for the callers of this function, though; now all callers
62 have to know that t needs to be passed itself as its first argument, so the
63 immediate value of the block would be transformed to:
7264
73 In so doing, we can fully define anonymous-t without any
74 self-references, and ditto with t. Note that in the actual
75 implementation of this feature, anonymous-t does not have a name in the
76 global namespace; it is fully anonymous and is only defined by its
77 value.
65 ! t t 1
7866
79 In order to accomplish this, the closure transformation code marks the
80 application of the function to itself as the application of a recursion
81 parameter, but otherwise performs the closure transformation normally.
82 In the above example, this then becomes:
67 But this is clunky; we don't want to have to transform every call site that
68 references t. Instead, we transform t to a partially-applied function, with
69 itself already applied (for brevity, B is used to represent the body of the
70 function t as given in the previous listing):
8371
84 : t = (\t x -> ? {
85 . eq x 0 => "ok\n"
86 . => t 0
87 }) $t
72 : t = (\$t x -> B) (\$t x -> B)
8873
89 With the dollar sign here being shorthand for is-recursive-parameter.
90 Then, in the code-generation stage, when the code generation visitor
91 sees an apply statement with a recursion parameter, it transforms:
74 Note that this can be very succinctly represented in Ubik bytecode, without
75 requiring a double-definition of the function, as the function must be its own
76 value:
9277
93 (f $f)
78 000 VALUE &B
79 001 APPLY 000 000
9480
95 Into a new top-level value inserted into the workspace and a reference
96 to that value, replacing the recursion parameter with a reference to the
97 same value.
81 This seems nice in practice, but there's one more sticky bit: the original
82 closure transformation. Let's take a slightly more complicated example:
9883
99 &f = f
100 ((const &f) ((const &f) (const &f)))
101
102 The closure transformation is maintained on f, allowing it to
103 self-recurse without any self-references.
104
105 This gets a little more complicated when there are multiple closed
106 parameters being applied. Take this useless function as an example:
107
108 {
109 : x = 8
110 : f = \y -> f x
84 : top = \x -> {
85 : inner = \y -> inner (+ x y)
86 ! inner
11187 }
11288
113 Both of these would be valid closure transformations for f:
89 Never mind that this is infinitely recursive: it's a nice simple example for
90 our syntactic transformations. The closure transformation takes inner from that
91 given in the previous listing to this:
11492
115 : f = (\f x y -> f x) *f x
116 : f = (\x f y -> f x) x *f
93 : inner = (\x y -> inner (+ x y)) x
11794
118 And their associated recusion transformations are:
95 Which works fine. The recursion transformation would then take it to:
11996
120 : anonymous-f = \f x y -> f x
121 : f = anonymous-f anonymous-f x
97 : inner =
98 (\inner x y -> inner inner (+ x y))
99 (\inner x y -> inner inner (+ x y))
100 x
122101
123 : anonymous-f = \x f y -> f x
124 : f = anonymous-f x anonymous-f
125
126 There are no statements here that look exactly like our (f *f) pattern
127 above, though, in the example where f is not the first closure
128 parameter. We end up with:
129
130 ((f x) *f)
131
132 And (f x) isn't extractable, because x is local. We also can't
133 successfully partially-apply this function to itself, because we would
134 have to apply the arguments ahead of the recursive application. We get
135 around this by forcing the recursive application to happen before any
136 other closure application.
137
138 Note that top level bindings do not need this transformation, because
139 they can reference themselves by name. So this function at the top
140 level:
141
142 ~ p
143 : f = \t -> f 0
144
145 Gets turned into the following pseudo-bytecode:
146
147 000 INPUT 0
148 001 LOAD "p:f"
149 002 CONST 0
150 003 APPLY 001 002
151
152 and the LOAD breaks the cycle.
102 This looks okay, but doesn't properly work, because the reference to inner in
103 the body of the function doesn't pass any of the closed-over parameters. What
104 we want