Defunctionalize the Continuation
pathsensitive.com
pathsensitive.com
In particular, my most recent project at work consisted of a system that takes a description of a computation from the user and successively performs optimisation passes on the description until we get a description of something that could run really efficiently, and then evaluating that efficient description into an actual computation that you really can run. If the successively-more-optimised descriptions are all defunctionalised to the greatest possible extent (in effect, deferring any concrete implementation details for as long as possible), you have much broader scope to optimise during later passes (since all the information "remains visible", not hidden away in function calls).
Moreover, this approach is extremely testable: initial algebras are built to be re-interpreted, so you can write reference evaluators for them; while defunctionalisation gives you a way of checking output without needing to run any pesky programs.
[0]: https://www.amazon.com/Advanced-Compiler-Design-Implementati...
Ow, we want the funk (we're gonna turn this mother out)
Give up the funk
Ow, we need the funk (we're gonna turn this mother out)
We gotta have that funkReferencing his "Typed Tagless Final Interpreters: Lecture Notes", it seems like a tagless-final interpreter is just a regular function whose result depends on the domain you intend to evaluate it in. It seems like the easiest thing to do is define a domain in which the tagless-final primitive operations map to an initial type, then serialize that. (So tagless-final seems at least as expressive as the initial representation, and by the "initial" concept, I would expect it to be no more expressive.)
https://www.cs.ox.ac.uk/jeremy.gibbons/publications/delivery... http://simonmar.github.io/bib/papers/haxl-icfp14.pdf https://www.staff.ncl.ac.uk/andrey.mokhov/selective-functors...
I always did these conversions in an ad-hoc fashion but this approach based on CPS + defunctionalization is definitely much clearer and easier on the brain!
recursive:
def fib(n):
n = min(n, 0)
if n > 1:
return fib(n-1) + fib(n-2)
else:
return n
CPS: def fib(n, kont):
n = min(n, 0)
if n > 1:
return fib(n - 1, lambda x: x + fib(n - 2, kont))
else:
return n
but then I get stuck at defunctionalizing. I can't figure out where the logic of combining values from the reduction step should go, or how to properly structure the continuation data structure. # BROKEN -- where do I combine the values?
def fib(n, kont):
n = min(n, 0)
if n > 1:
return fib(n-1, {"val": n, "next": kont})
else:
return kapply(kont, n)
def kapply(kont, n):
if kont: return fib(kont["val"] - 2, kont["next"])
return n def my_filter_recursive(l, f):
if l:
x, *xs = l
if f(x):
return [x] + my_filter_recursive(xs, f)
else:
return my_filter_recursive(xs, f)
else:
return []
In Continuation Passing Style it becomes: def my_filter_cps(l, f, kont):
if l:
x, *xs = l
if f(x):
my_filter_cps(xs, f, lambda y: kont([x] + y))
else:
my_filter_cps(xs, f, lambda y: kont(y))
else:
kont([])
Then I think with defunctionalisation it becomes: def my_filter_defun(l, f, kont):
if l:
x, *xs = l
if f(x):
my_filter_defun(xs, f, KontAdd(x, kont))
else:
my_filter_defun(xs, f, KontSkip(kont))
else:
kont.apply([])
class KontAdd:
def __init__(self, var, kont):
self.var = var
self.kont = kont
def apply(self, acc):
self.kont.apply([self.var] + acc)
class KontSkip:
def __init__(self, kont):
self.kont = kont
def apply(self, acc):
self.kont.apply(acc)
class KontPrint:
def apply(self, acc):
print(acc)
Is that right? It seems that we've basically just moved the recursion from one place to another.Yes, the point is to move the recursion from the code into the data structures. These data structures can then be stored and retrieved conveniently. It's a technique for making micro eDSLs to solve common problems.
Having said that, it's a lot more applicable to some languages than others. Python, lacking algebraic data types and static typing, does not make it very convenient to use this technique.
This is right, although you could replace KontSkip(kont) with just kont. Defunctionalization does not just "move the recursion from one place to another." Rather, it doesn't change the recursion at all.
Also, your example is actually harder than the one in the talk, because it can involve an arbitrary amount of work after the last call to my_filter_recursive.
Like in the example in the talk, you still need merge the various calls to apply, and use tail-call elimination.
Merging:
def apply(kont, acc):
if isinstance(kont, KontAdd):
apply(kont.kont, [kont.var] + acc)
else:
print(acc)
You will now actually need two levels of tail-call elimination because there are two recursions, rather than one as in the example in the talk. apply becomes this: def apply(kont, acc):
while True:
if isinstance(kont, KontAdd):
kont = kont.kont
acc = [kont.var] + acc
else:
print(acc)
break
You can then inline this into my_filter_cps and do tail-call elimination again. def my_filter_defun(l, f, kont):
while True:
if l:
x, *xs = l
if f(x):
l = xs
kont = KontAdd(x, kont)
else:
l = xs
else:
acc = []
while True:
if isinstance(kont, KontAdd):
kont = kont.kont
acc = [kont.var] + acc
else:
print(acc)
breakIt's not as fast as a plain iterative version would be, though, and the article gave me some ideas to experiment with.