We use cookies to distinguish you from other users and to provide you with a better experience on our websites. Close this message to accept cookies or find out how to manage your cookie settings.
To save content items to your account,
please confirm that you agree to abide by our usage policies.
If this is the first time you use this feature, you will be asked to authorise Cambridge Core to connect with your account.
Find out more about saving content to .
To save content items to your Kindle, first ensure [email protected]
is added to your Approved Personal Document E-mail List under your Personal Document Settings
on the Manage Your Content and Devices page of your Amazon account. Then enter the ‘name’ part
of your Kindle email address below.
Find out more about saving to your Kindle.
Note you can select to save to either the @free.kindle.com or @kindle.com variations.
‘@free.kindle.com’ emails are free but can only be saved to your device when it is connected to wi-fi.
‘@kindle.com’ emails can be delivered even when you are not connected to wi-fi, but note that service fees apply.
There are two almost separate issues to be addressed when we consider polymorphic languages: How to perform polymorphic binding-time analysis, and how to specialise polymorphic functions. We address both here.
Strachey identified two flavours of polymorphism [Str67] which he styled parametric and ad hoc. We will only consider parametric polymorphism, as arises in the widely used Hindley-Milner type system, for example. As ad hoc polymorphism may be reduced to parametric polymorphism by introducing higher-order functions [WB89], this decision is consistent with the thrust of the thesis where we have been considering a first-order language only.
A polymorphic function is a collection of monomorphic instances which, in some sense, behave the same way. Ideally, we would like to take advantage of this uniformity to analyse (and perhaps even specialise) a polymorphic function once, and then to use the result in each instance. Up to now the only work in polymorphic partial evaluation has been by Mogensen [Mog89]. However, with his polymorphic instance analysis each instance of a polymorphic function is analysed independently of the other instances and, as a result, a single function may be analysed many times.
To capture the notion of uniformity across instances Abramsky defined the notion of polymorphic invariance [Abr86]. A property is polymorphically invariant if, when it holds in one instance, it holds in all. Abramsky showed, for example, that a particular strictness analysis was polymorphically invariant. Unfortunately this does not go far enough. Polymorphic invariance guarantees that the result of the analysis of any monomorphic instance of a polymorphic function can be used in all instances, but not that the abstraction of the function can. An example of this distinction appears in [Hug89a].
There seems to be a fundamental dichotomy in computing between clarity and efficiency. From the programmer's point of view it is desirable to break a problem into sub problems and to tackle each of the sub problems independently. Once these have been solved the solutions are combined to provide a solution to the original problem. If the decomposition has been well chosen, the final solution will be a clear implementation of the algorithm, but because of intermediate values passing between the various modules, whether they are functions and procedures or separate processes connected by pipes, the solution is unlikely to be as efficient as possible. Conversely, if efficiency is considered paramount, many logically separate computations may need to be performed together. As a consequence, the algorithm will be reflected less directly in the program, and correctness may be hard to ascertain. Thus, in most programs we find a tradeoff between these conflicting requirements of clarity and efficiency.
An extreme form of modularisation is to write programs in an interpretive style, where flow of control is determined by stored data. Programs in this style are comparatively easy to prove correct and to modify when requirements change, but are well known to have extremely poor run-time behaviour–often an order of magnitude slower than their non-interpretive counterparts. Because of this, the interpretive style tends to be used infrequently and in non time-critical contexts. Instead, flow of control is determined deep within the program where a reasonable level of efficiency may be obtained.
The static projection tells us which part of a function's argument will be present during partial evaluation. In any particular call of the function, this part of the argument is used in the production of a residual function. However, this still leaves the question: which part of the argument should the residual function be given at run-time? Obviously we could pass the whole argument if we wanted to, but we can do a lot better. After all, we assume that the partial evaluator will have taken the static part of the argument into account in producing the residual function. It ought to be unnecessary to supply the residual function with the same information all over again.
We need a way to select the run-time information. The original argument to a function ƒ must be factorised, or decomposed, into static and dynamic factors, and this factorisation should be as complete as possible. That is, the amount of static information which is also regarded as dynamic should be minimised. Then, when we pass the dynamic argument to the residual function, we will be passing as little information at run-time as possible. There are, of course, many possible factorisation methods. Some produce an exact decomposition while others do not in that they contain extra junk.
We will look at two methods in this chapter. While the first does not produce an exact factorisation of the original argument, it is based on very familiar constructions and is interesting in its own right. The second method, which is exact, arises as a generalisation of the first, and provides a practical application of some fairly deep mathematics.
The equations for mix assume that it is operating on a two argument function where the first argument is static and the second dynamic. This is the canonical case. In practice we cannot hope that all functions will turn out this way. For example, a function may have many arguments, the first and third being static, say. Alternatively, a single argument may have both static and dynamic parts. We need a framework for reducing the general case to the canonical case.
We can simplify the general case by requiring that all functions have exactly one argument. In first-order languages this is no real restriction. Functions must always be applied to all their arguments, so we can just express them as a single tuple. The next stage is to factorise this single (composite) argument into two parts, the static and the dynamic. We use the results of binding-time analysis to control the factorisation.
Note that, even though functions will only have one argument, we will still loosely describe them as having many. For example, we will talk of a function f (x, y) = … as having two arguments when this is appropriate.
Motivation
For the present we will focus our attention on the static part of the argument. To select the static part, we use a function from the argument domain to some domain of static values. If we make the static domain a sub-domain of the original we can simply “blank out” the dynamic part of the argument and leave the static part unchanged.
This thesis is submitted in partial fulfillment of the requirements for a Doctor of Philosophy Degree at Glasgow University. It comprises a study of partial evaluation, with the thesis that domain projections provide an important theoretical and practical tool for its development.
Our aim, therefore, is not so much to describe a stronger or more robust partial evaluator than has been achieved hitherto, but to improve our understanding of the partial evaluation process. Because of this much of the thesis is theoretical. However, to demonstrate that the ideas are also practical, they have been implemented. As a result, the chapters tend to alternate between theory and practice.
In Chapter 1 we explore the principles of partial evaluation and in Chapter 2 we study the algorithms and techniques used. In Chapters 3 and 4 we address the issue of binding-time analysis. Chapter 3 contains theory, including the relationship between congruence in binding-time analysis and safety in strictness analysis, and Chapter 4 the practice-the equations used in an implementation and a proof of their correctness. In Chapter 5, we discuss the nature of residual functions and their run-time arguments, and develop a theoretical framework based on dependent sums of domains. The practical implications of this are seen in Chapter 6 where we bring the material from the previous chapters together in a working projection-based partial evaluator. In Chapter 7 we turn our attention to polymorphism to address some of the issues it raises, and Chapter 8 concludes the thesis. The appendices which follow contain annotated listings of the programs used to construct the final polymorphic partial evaluator.
Our first view of a concurrent process is that of a machine where every detail of its behaviour is explicit. We could take as our machine model automata in the sense of classical automata theory [RS59], also known as transition systems [Kel76]. Automata are fine except that they cannot represent situations where parts of a machine work independently or concurrently. Since we are after such a representation, we use Petri nets [Pet62, Rei85] instead. This choice is motivated by the following advantages of nets:
Concepts. Petri nets are based on a simple extension of the concepts of state and transition known from automata. The extension is that in nets both states and transitions are distributed over several places. This allows an explicit distinction between concurrency and sequentiality.
Graphics. Petri nets have a graphical representation that visualises the different basic concepts about processes like sequentiality, choice, concurrency and synchronisation.
Size. Since Petri nets allow cycles, a large class of processes can be represented by finite nets. Also, as a consequence of (1), parallel composition will be additive in size rather than multiplicative.
An attractive alternative to Petri nets are event structures introduced in [NPW81] and further developed by Winskel [Win80, Win87]. Event structures are more abstract than nets because they do not record states, only events, i.e. the occurences of transitions. But in order to forget about states, event structures must not contain cycles. This yields infinite event structures even in cases where finite (but cyclic) nets suffice.