Transcript
Page 1: Creating efficient programs by exchanging data for procedures

Comput. Lang. Vol. 14, No. 1, pp. t 1-23, 1989 0096-0551/89 $3.00 + 0.00 Printed in Great Britain. All rights reserved Copyright © 1989 Pergamon Press plc

C R E A T I N G E F F I C I E N T P R O G R A M S B Y E X C H A N G I N G

D A T A F O R P R O C E D U R E S t

JOHN FRANCO and DANIEL P. FRIEDMAN Department of Computer Science, Indiana University, Bloomington, IN 47405, U.S.A.

(Received 6 July 1988; revision received 3 October 1988)

Abstract--We present a programming style which fosters the rapid development of programs with low asymptotic complexity. The crucial idea behind the new programming style is that mutually recursive procedures, each assigned the task of returning the solution of a subproblem of the given problem, are constructed directly from the problem instance. That is, the input data is compiled into procedures.

Scheme Lisp Programming languages Complexity Efficiency

1. I N T R O D U C T I O N

Traditionally, software developers have seen data as an entity to be entirely processed by a program. However, data can be viewed as instructions for building at least some of the program text. For example, the string matching algorithm in [1] syntactically transforms input strings to control structures that become part of the computat ional process. As is the case in string matching, programs where data is compiled can be both efficient and concise.

In this paper we present the beginnings of a programming methodology of wide applicability in which some or all input data is syntactically transformed to compiled code. The transformations may occur as late as run-time. They are particularly suited to problems that can be solved by subproblem decomposition. Each subproblem is regarded as a three-state object which communi- cates with other objects. Generally, but not exclusively, the communications are requested for the solution or partial solution of more primitive subproblems. The states are: (1) waiting for the first request; (2) processing that request; and (3) having found the solution to the subproblem. The objects are modeled as procedures and each state change results in a change in procedure definition. The programmer specifies only a template for procedure definitions and communications links. That template is used to transform at least some of the data to an interacting network of procedures, possibly changing with time due to the generation of requests for subproblems that are implied by the data.

Programs developed under the proposed methodology can be both concise and efficient in time and space. Conciseness is due to the reduction of the programming effort to templates. Efficiency of time is due to the three-state model, which can prevent the same subproblem from being solved twice, and to the ability of the problems to do partial computat ion on compiled data arguments in the presence of non-compiled data arguments. Efficiency of space is due to the property that no space need be used on procedures representing subproblems for which there are no requests.

Unfortunately, there is no existing programming language that can accommodate the methodology as we envision it. However, the macro-expansion facility of Scheme known as extend-syntax (see [2-5]) allows partial implementation. This facility is inefficient for our purposes since it is not designed for our application. Hence, for the rest of this paper, we make use of ex tend-syntax only for illustrative purposes and ignore the computat ion time of the ex tend-syn tax facility. Using ex tend-syn tax we have developed concise and efficient programs for numerous graph problems (e.g. the Shortest Path problem), Object-Simulations, the Topo- logical Sort problem, and others. Examples of such programs are presented later.

tThis report is based on work supported by Air Force Office of Scientific Research under Grant No. AFOSR-84-0372 and by NSF Grants DCR 85-01277 and DCR 854)3279.

II

Page 2: Creating efficient programs by exchanging data for procedures

12 JOHN FRANCO and DANIEL P. FRIEDMAN

Traditionally, time required for compilation has been regarded as unimportant since only one compile is necessary using conventional programming techniques. This time is significant using our approach, however, if re-compilation is necessary for each change in data (as is the case for many graph theory problems). It may not be significant if there is both a compiled data argument and a non-compiled data argument. For example, consider the problem of simulating a finite state acceptor. Such simulations require a description of the machine and one or more inputs to the machine as data. Only the description data is compiled. Hence, re-compiles are only necessary when machine descriptions change, and compilation time can be amortized over all simulations of the same machine. Furthermore, because most of the computational effort is accounted for during compilation, the run-time of each simulation is extremely fast. This property, which holds for many problems, is similar to the property than motivates research on partial computation (see e.g. [6-10]).

At present, the methodology is ad hoc and underdeveloped. We have found little resistance to applicability in some problem domains and considerable resistance in others. The potential for its success is good, however, for the following reasons:

• The treatment of elements of computation as objects embedded in a communications network is well understood.

• It can be extended to admit concise, efficient code for a wide variety of problems. • Partial computation is, to some extent, built into the methodology. • The three-state mechanism fosters efficient solutions to some problems. • The code between the assignment statements that change procedure definitions can

be functional. The "piecewise" functional nature of procedures may lead to improved verification.

In summary, it may be possible for concise and efficient programs to be constructed by means of the techniques that are illustrated in this paper. This goal is similar to that of researchers studying partial computation and functional transformations. Before going further we touch on the work of these groups.

Functional programming has been developed with the aim of writing easily verifiable programs (we call this comprehensibility). Comprehensibility is achieved, for the most part, by attenuating considerations of state and control. The problem with functional techniques is that programs so written, in conventional style and for conventional hardware, usually are neither as efficient as possible nor understood in terms of their time and/or space complexity.

One possible remedy is the concept of transforming a functional program to an equivalent, possibly non-functional program of improved complexity [11-15], The transformation itself can be interactive. The idea is to have a programmer produce a comprehensible program and let the transformation take care of the complexity issues. The transformation need not be efficient since it is performed only once, just prior to compilation.

Although remarkable successes have been achieved, there are several stumbling blocks to the transformation approach as now practiced. First, one or more "eureka" steps is required in order to achieve success. Second, although the problem of producing syntactic transformation rules such as "append ~ rplacd" seems tractable, the problem of producing transformation rules which are semantically generated is a hard one to solve correctly since an enormous amount of rule inter- action must be taken into account. For example, consider the problem of producing an efficient Union-Find algorithm. It is easy to find an optimal algorithm for doing Union and an optimal algorithm for doing Find, but neither of the two is optimal for solving the Union-Find problem [16]. Third, the transformation approach cannot be complete since the problem of determining complexity is undecidable [17].

Partial computation of a computer program is by definition [7] "specializing a general program based upon its operating environment into a more efficient program." The objective is to reduce run-time complexity by re-using the results of previous partial computations. The idea is to regard programs as data and perform an analysis which allows computation to proceed as much as possible with no or partial actual data. Analyzed programs may be manipulated in order to continue the partial computation as much as possible.

Page 3: Creating efficient programs by exchanging data for procedures

Creating etficient programs by exchanging data for procedures 13

Our approach is different from both the partial computation and functional transformation approaches. Partial computation regards programs as data to be manipulated. Data compilation, however, regards data as programs. Doing so results in some form of partial computation in many cases. Functional transformations convert functional programs to efficient, possibly non-functional programs. The transformations require one or more "eureka" steps. Data compilation also requires a "eureka" step to develop a program that replicates in precisely the right way. It relies on the creative effort of the programmer to design a solution of low complexity. We believe our tools, under data compilation, will make this creative effort relatively easy because the metaphors behind their use will change little from one problem to the next.

The remainder of this paper contains three sections. In the next section we show how to construct code from input data at compile-time using the macro-expansion facility, known as extend-syntax [3]. We use the example of constructing a finite state acceptor to illustrate the ability of our techniques to yield comprehensible programs. This example also has both a data component that does get compiled and one that does not. We use the example of Topological Sort to illustrate the three-state paradigm. We use the example of finding cutpoints in a graph to illustrate a complicated use of our ideas. In Section 3 we consider the solution to sample dynamic networks which arise in Dynamic Programming. In Section 4 we present conclusions.

2. C O N S T R U C T I N G CODE FROM INPUT DATA AT C O M P I L E - T I M E

Conventional compilers lack the sophistication needed to optimize complexity. In Lisp-systems, on the other hand, macro-sublanguages provide a facility for extending the compiled language. With a macro-facility we can design algorithms that translate input data directly into efficient code. Such a solution is particularly preferable when macro definitions are easy to design as with extend-syntax [3-5].

2.1 Syntactic extension

We briefly sketch the use of this macro expansion facility in constructing what we call macro-procedures. For simplicity, the examples considered in this subsection do not make use of the three-state paradigm.

Consider the problem of testing set membership (without recursion) in a small finite set of symbols. The following is a macro-procedure for solving this problem. This procedure, which uses a curried eq? called c-eq? defined by (lambda (x) (lambda (y) (eq? x y))) , is intended only for illustrative purposes:

(extend-syntax (member-in-set?) [(member-in-set? n...)

(let ([n (c-eq? 'n) ] . . . ) (lambda (y) (or (n y) . . . ) ) ) ] )

The body of this program, given the input (member-in-set? a b c), expands to

(let ([a (c-eq? 'a)] [b (c-eq? 'b)] [c (c-eq? 'c)])

(lambda (y) (or (a y) (b y) (c y)))),.

Consider the characteristic function cf = (member-in-set? a b c). The call (cf 'a) returns true. This happens as follows: y gets the value 'a and or is invoked. At some point procedure a is invoked with argument 'a. But this procedure is simply a test with symbol 'a and the argument y which has value 'a. Hence the test returns true as does the or. The call (cf 'd) returns false because no procedure matches with 'd.

As a demonstration of the level of comprehensibility that is possible to compiling data, consider the problem of writing a finite state acceptor. Suppose the definition of a specific finite state acceptor is an initial state followed by a list of triples (S, A, P) where S is a state identifier; A is true if the state S is an accepting state and false otherwise; and P is a list of pairs (d, S') which defines the transitions to states S' given input symbol d. A macro-procedure for this

Page 4: Creating efficient programs by exchanging data for procedures

14

(letrec

JOHN FRANCO and DANIEL P. FRIEDMAN

(ex tend-syn tax ( fsa) [ ( f s a i n i t - s t a t e ([Sa f i n a l [a Sb] . . . ] . . . ) ) (letrec

([Sa (lambda (I) (case (car i) [a (Sb (cdr i))] ... [$ final] [else #f]))]

• . .)

init-stat e) ] )

Fig. l(a). A Finite State Accepter.

(define test (lambda ()

(let ( [machine (fsa

qO ([qO #t [a qO] [b ql]] [ql #t [b ql] [a q2]] [q2 #f [a q2] [b q2]]))])

(machine '(a a a b b c $)))))

Fig. l(b). An example using the program of Fig. l(a).

([qO (lambda (i) (case (car i) [a (qO (cdr i))] [b (ql (cdr i))] [$ #t] [else #f]))]

[ql (lambda (i) (case (car i) [b (ql (cdr I))] [a (q2 (cdr i))] [$ #t] [else #f]))]

[q2 (lambda (i) (case (car I) [a (q2 (cdr i))] [b (q2 (cdr i))] [$ #f] [else #f]))])

qo)

Fig. l(c). The expansion of machine specified in Fig. l(b).

problem is given in Fig. l(a). This procedure constructs a finite state machine simulator, using extend-syntax, from a machine specification in the form given above. It is essentially a one line program which expands to an n line program where n is the number of states. Each line is a case statement that leads to three outcomes: a call to the prodedure corresponding to the next state depending on the next input symbol; termination of computation when the special end-of-string symbol $ is encountered; termination when an illegal symbol is encountered. Once constructed, the simulator takes a string representing the machine's input as argument• It's output is true when the simulated machine accepts its input.

The program of Fig. 1 (a) simulates a finite state machine optimally modulo the compilation time. It is purely functional. If called as in the example of Fig. l(b), as many simulations as desired may be run on one compilation. Hence, in this case compilation time is not an important time complexity factor. The reader is encouraged to rewrite Isa to take arguments of quoted data, using a purely functional paradigm.

The code for fsa may be used to simulate any finite state accepter. The expansion for the machine simulated by the example of Fig. l(b) is given in Fig. l(c). The example illustrates that, with a macro-procedure, the input symbols become program identifiers which are bound to producers as in Katayama [18]. These procedures may be regarded as objects which communi- cate with each other. We used extend-syntax to construct procedure pointers and "wire" the communication links at compile-time.

Our next macro-procedure example, a solution to Topological Sort, is one of the most elementary macro-procedures that uses the three-state paradigm.

2.2 Macro-procedure solution to Topological Sort

Topological Sort is the problem of finding a total order of elements that are partially ordered. This problem is fundamental and is described at length in [19]. Let the input partial order have the syntax implied by the following example:

([a (b c e)] [b (d f)] [c (b)] [d () ] [e (b f)] [f (d)]).

Page 5: Creating efficient programs by exchanging data for procedures

Creating efficient programs by exchanging data for procedures

Fig. 2

15

This means a must be preceded by b, c, and e; b must be preceded by d and f; and so on. Each pairing of an element with a member of its associated list is said to be a precedence relation. In the example above there are nine precedence relations and a total ordering is d, f, b, c, e, a.

Regard each distinct element of the input partial order as an object and each relation from an element to a member of its adjacency list as a communication link between the elements. Diagram the objects as nodes of a graph and the communication links as directed edges. For the input example the graph looks like Fig. 2.

The macro-expansion facility allows the macro-procedure to be shared by all nodes. The code describes the local interactions needed to determine that the element associated with a node is ready to be appended to the topologically sorted list. A node is ready when all neighbors along outgoing edges have been appended to the list. This can be determined by sending signals to all outgoing neighbors. These signals are returned exactly when the element associated with a node receiving such a signal is appended to the list. When a node receives returned signals from all its outgoing neighbors it is ready to be appended to the list. In the macro-procedure, signals sent to other nodes correspond to calls to macro-procedures associated with outgoing neighbor nodes. Since the neighbors, along outgoing edges, of an appended node must also have been appended, there is no need for that node to send signals more than once. Therefore, when an element is appended to the sorted list, the associated macro-procedure re-defines itself to return "done". If a node that is sending signals receives one, there is a cycle and no topological sort exists. Thus, when a macro-procedure is first called, it temporarily re-defines itself to return an error message and halt.

The complete macro-procedure for Topological Sort is shown in Fig. 3. This macro-procedure builds out-list, the list representing the topological sort of the input partial order. The supporting procedures open-list, close-list, and add-to-l is t are straightforward and self explanatory. They represent procedures that should be built-in since they perform the basic function of efficiently appending to an unshared list. Because of this and the fact that they have no impact on the semantics of the solution to Topological Sort they have been moved to Appendix A for reference. The expansion of the following input to topo is given in Appendix A:

(topo ([a (b c e)] [b (d f)] [c (b)] [d () ] [e (b f)] [f (d)])).

We make the following observations about topo.

• The complexity of topo is O(n) for implementations of Scheme where identifier lookup is assumed to take constant time, and compilation and macro expansion time is linear in n. This is optimal to within a constant factor.

• None of the input symbols are quoted because each is regarded as a procedure. The data is a collection of procedure identifiers and these identifiers may clash with those given in the code. We have avoided such clashes when choosing identifiers for our data. Avoiding clashes is rendered unnecessary by using hygienic expansion [4, 20] or syntactic closures [21].

• The program does not contain a test. • The program is not functional. In fact it goes in the opposite direction as it is actually

self-modifying. There are two reasons we have chosen to make the code self-modifying. First, doing so eliminates a test. The procedure topo only changes twice during its lifetime but the test, if implemented, would be executed up to O(n) times. Second, the nature of the self-modification is, we believe, well understood: the modification is to replace a procedure

C L . 14 i - - [ I

Page 6: Creating efficient programs by exchanging data for procedures

16 JOHN FRANCO and DANIEL P. FRIEDMAN

(extend-syntax (topo) [(topo ([n (m . . . ) ] . . . ) ) (let ([out-list (open-list)])

(letrec ([n (lambda ()

(set! n (err-fun 'n))

(m) ...

(add-to-list 'n out-list)

(set! n done-fun))]

• ..)

(n) ... (close-list out-list)))])

Fig. 3. Amacro-proceduresolution to Topolo~cal So~.

by the value it computes. Consequently, we do not believe this macro-procedure is subject to the pitfalls of general self-modifying code. The code for topologically sorting a partially ordered set found in [19] uses a sophisticated data structure in order to achieve linear complexity, but here data structure maintenance is hidden. The macro-procedure of Fig. 3 can be modified to construct out-list lazily by means of call-with-current-continuation (call/co). Essentially all the changes needed are to the pseudo-system procedures of Appendix A. The result is the code of Appendix B. This code is interesting because it is an effective example of the use of streams in an imperative environment. We have been able to use this idea in several other examples and we believe it will affect the way assignments are related to comprehensibility and complexity issues.

As stated, the macro-procedure topo is self-modifying. This controversial stand is justified because the nature of self-modification is under control and results in the elmination of a test. An alternative is to create a three state variable local to each procedure which is used to record the state the procedure is in. Calling programs are simply diverted to the appropriate section of code based on the value of the variable. For some problems it makes sense to retain local state in each procedure. The next example illustrates the use of local state.

2.3 Macro-procedure solution to Shortest Path

The following is the statement of the Shortest Path problem: given a graph G = (V, E) with positive edge weights w : E - - , N ÷ and a distinguished vertex v~ in V, find the shortest path lengths from v, to all other vertices v in V. Let the input to be a list of lists, each containing a vertex v in V and a list of pairs of vertices v' adjacent to it and the weight of the edge <v', v">. Let the distinguished vertex be the first one in the first list. The following is a sample input

((vl (~v2 10) (v3 5))) (v2 ((v3 3) (va 4) (v5 3))))

(v3 ((v2 4) (v, 6) (v5 10))) (v, ()) (v5 ())).

Our solution to this problem is patterned after the algorithm in [16]. Let the output be a list of vertex-value pairs where the value is the length of the shortest path from v~ to the vertex specified in the pair. The following data structures are maintained or implied:

(1) A list S of vertex-value pairs--The values are the lengths of the shortest paths from v~ to the corresponding vertices. S is initially null. A new vertex-value pair is added to S on every iteration of f ind-next-node-in-L below. In the implementation below, S is implied: nodes stream out.

(2) An ordered list L of vertex-value pairs--The vertices in L are not in S, the values are the lengths of the shortest paths from v, to the corresponding vertices such that the paths lie wholly among vertices in S. The list ordering may be chosen to maximize the efficiency of finding a minimum entry in L. We avoid discussion of all the possible

Page 7: Creating efficient programs by exchanging data for procedures

Creating efficient programs by exchanging data for procedures 17

ways to set up L. Every time a vertex w is put into S one of 3 things happens to each neighbor of w:

(a) if the neighbor m is not in L then it is put there with the value of the edge <w, m >; (b) if m is in L then the value associated with m is changed to the minimum of the old

value and the value associated with w (in S) plus the value of the edge <w, rn>; and (c) if the neighbor m has already been added to S then it is not put into L.

The body of the program works roughly as follows: The distinguished vertex is put into L with the value 0 using Put-L. The pairs in L are searched for the one having least value using min-node- in-k. This pair is popped from L, is output, and the neighbors of the vertex specified in the pair are either placed into L or their associated values are updated according to the conditions in (2) above. This continues repetitively until L is empty.

For each vertex there is a node n with two arguments x and y where x is a node label or switch and y is an integer. It x is 'p ick-node (node x is being put into S from L), then if the value of neighbor m is 10,000 (what we deem to be infinity), then rn is put into L with the value of the edge <n, m > plus the value of node n (which is in S) using Put-L. Otherwise, if the value of node m is greater than the sum of the last sentence (it has already been put in L) then the value of node m is changed to the value of node n plus the edge weight from n to m. This entry in L may also be moved depending on how L is set up. The code below does not take such movement into account. This is repeated for all neighbors of n. Otherwise, if x is 'set-val then the value of x is set to y. I f x is 'get-val, the value of the called node is returned. The code is shown in Fig. 4 minus specification of the functions Put-L and min-node- in-L which the reader may easily supply.

This code may be used on the following sample input:

(short ((vl ((v2 10) (v3 5))) (v2 ((v3 3) (v4 4) (v5 3)))

(v3 ((v2 4) (8 4 6) (v5 10))) (v4 ()) (v5 ()))).

We make the following three points about short.

• The code uses local state in place of arrays to store temporary shortest path values. There may not be any efficiency advantage doing this but the elimination of arrays makes the program read more like a function specification: that is, array bounds do not need to be specified.

(extend-syntax (short) [(short [(n [(m v) ... ]) ... ])

(letrec ([n (let ([value-of-n i0000])

(lambda (x y) (cond [(eq7 x 'pick-node)

(let ([value-of-m (m 'get-val 0)]) (cond [(> value-of-m (+ value-of-n v))

(m 'set-val (+ value-of-n v))] [(eq? value-of-m I0000) (Put-L m)]))

• , .]

[(eq? X 'set-val) (set! value-of-n y)]) value-of-n))] ... )

(let ([distinguished-node (car (list n ... ))]) (distinguished-node 'set-val O) (Put-L distinguished-node) ((rec find-next-node-in-i

(lambda () (let ([w (min-node-in-L)])

(output w) (w 'pick-node 0) (find-next-node-in-L)))))))])

Fig. 4. A macro-procedure solution to Shortest Paths.

Page 8: Creating efficient programs by exchanging data for procedures

18 JOHN FRANCO and DANIEL P. I'-RIEDMAN

• The macro-procedures do not re-define themselves. In fact the only state changes are to local variables (excluding PUT-L).

• This example shows the strong relationship between our programs and object-oriented programming. Using a macro-facility such as ex tend-syntax our "objects" are "wired" together with relatively little specification.

3. P A R T I T I O N PROBLEM: E X A M P L E OF D Y N A M I C NODE S T R U C T U R E

The preceding examples use ex tend-syntax primarily because the communication structure between nodes is not known before run-time, the communication structure between nodes is complex, and there is a one-to-one correspondence between nodes and the elements of each input. In applications of Backtracking, Divide-and-Conquer, Branch-and-Bound and Dynamic Program- ming, the communication structure is a tree with a number of nodes that can be exponential in the number of input elements. Frequently, however, only a small subset of nodes needs to be visited in order to obtain a solution to the given problem. If efficient implementations are to be obtained in these cases the unnecessary nodes must not be created. It follows that nodes must be constructed on-the-fly. In this mode extend-syntax and Iotroc reduce to lazy cons. The result is efficient code for a variety of problems. Below is cons$, our name for lazy cons. We leave cars and cdr$ to the reader.

(extend-syntax (cons$) [(cons$ a d) (letrec

(rn (lambda () (set! n (let ([v a]) (lambda () v))) (n))]

[m (lambda ( ) (set! m (let ([v d]) (lambda ( ) v))) (m))])

(cons (lambda () (n)) (lambda () (m))))])

Consider, for example, the Partition problem (see ([22]). An instance of the Partition problem is a positive integer B and a set E of elements, each having a positive integer value. The problem is to determine whether there exists a subset of E with values that sum to B. We represent E as a stream of integers. The program sl ide-merge below takes a stream S of Partition subproblems and a value n as its arguments. We regard the subproblems in S in the same way as nodes above. As long as an unsolved subproblem exists in S, no solution to the given instance has been found, and there is another unconsidered element e in the stream representing E, then another stream of subproblems is created by merging S with the stream of positive valued subproblems obtained by subtracting the value of e from each subproblem in S. If some subproblem turns out to have 0 value then processing is terminated with the result that a partition exists, and if S becomes empty then no solution exists. The code for the Partition problem is in Fig. 5.

A sample test for partition is

(partition (cons$ 7 (cons$ 6 (cons$ 4 (cons$ 2 ' ( ) ) ) ) ) 15).

Although this test uses only a finite stream, the procedure partition works for infinite streams. In this case it terminates only when a solution is found.

4. C O N C L U S I O N S

We have presented the beginnings of a programming methodolodgy based on the notion of data compilation. We have used the existing facilities of ex tend-syntax and letrec to illustrate our ideas. Writing programs in this style encourages low asymptotic complexity without sacrificing elegance. Moreover, determining the complexity of programs written in this style seems straight- forward partially because the overhead of data structure maintenance is eliminated.

Two important areas of future work are the formalization of our ideas and the implementation of an efficient extend-syntax like facility which can handle dynamic networks. The trick of

Page 9: Creating efficient programs by exchanging data for procedures

Creating efficient programs by exchanging data for procedures

(define slide-merge (lambda (n S) (letrec ([next-node

(lambda (SI $2) (cond [(null? S2) SI] [else (let ([x (cars Sl)] [y (- (cars $2) n)])

(cond [(< y O) (next-node SI (cdr$ $2))] [(< x y) (cones x (nex t -node (cdr$ $1) S2))] [(= x y) (cons$ x (next-node (cdrS SI) (cdr$ S2)))] [else (cones y (next-node $1 (cdr$ $2)))]))]))])

(next-node S S))))

(define partition (letrec ([partition-stream

(lambda (E" S) (let ([NEXT (slide-merge (cars E') S)])

(or (zero? (cars NEXT)) (partition-stream (cdr$ E') NEXT))))])

(lambda (E B) (partition-stream E (cones B '())))))

Fig. 5. A macro-procedure solution to the Partition problem.

19

Section 3 is useful in a small number of problems. An extensible, lexically scoped, mutual recursion is needed to handle changing networks. When fully developed, the methodology is expected to be applicable to a wide variety of combinatorial problems.

R E F E R E N C E S

1. Knuth D., Morris J. H. and Pratt V. R. Fast pattern matching in strings. SIAM J. Comput. 6, 323-349 (1977). 2. Dybvig R. K. The Scheme Programming Language. Prentice-Hall, Englewood Cliffs, N.J. (1987). 3. Kohlbecker E. Syntactic extensions in the programming language LISP. Ph.D. dissertation, Indiana University (1986). 4. Kohlbecker E., Friedman D. P., Felleisen M. and Duba B. Hygienic macro extension. Proc. 1986 ACM Conf. on Lisp

and Functional Programming, pp. 151-161 (1986). 5. Kohlbecker E. and Wand M. Macro-by-example: Deriving syntactic transformations from their specifications.

Conf. Rec. 14 ACM SIGACT-SIGPLAN Symposium on Principles of Programming Languages, Munich, pp. 77 84 (1987).

6. Ershov A. P. On the essence of compilation. In Formal Description of Programming Concepts (Edited by Neuhold E. J.), pp. 391-418. North-Holland, Amsterdam (1978).

7. Futamura Y. Partial computation of programs. Proc. RIMS Symposia on Software Science and Engineering, Kyoto, Japan. Springer-Verlag LNCS no. 147 (1982).

8. Jones N. D., Sestoft P. and Sondergaard H. An experiment in partial evaluation: The generation of a compiler generator. Proc. 1st Int. Conf. on Rewriting Techniques and Applications, Dijon, France. Springer-Verlag LNCS no. 202, pp. 124-140 (1985).

9. Lombardi L. A. Incremental computation. In Advances in Computers (Edited by Alt F. L. and Rubinoff M), Vol. 8, pp. 247-333. Academic Press, New York (1967).

10. Turchin V. F. Program transformation by supercompilation. Proc. Programs as Data Objects, Copenhagen, Denmark. Springer-Verlag LNCS no. 217, pp. 257-281 (1985).

11. Burstall R. M. and Darlington J. A transformation system for developing recursive programs. J. ACM 24., 44-67 (1977).

12. Darlington J. and Burstall R. M. A system which automatically improves programs. Acta Informatica 6, 41-60 (1976).

13. Darlington J. Program transformation. In Functional Programming and its Applications (Edited by Darlington J., Hendersen P. and Turner D.). Cambridge University Press (1982).

14. Guttag J. V., Horning J. and Williams J. FP with data abstraction and strong typing. Proc. 198I Conference on Functional Programming, Languages, and Computer Architecture, pp. 11-24. ACM, New York (1981).

15. Scherlis W. L. Program improvement by internal specification. Conf. Rec. 8th ACM SIGACT-SIGPLAN Symposium on Principles of Programming Languages, Williamsburg, Va, pp. 41-49 (1981).

16. Aho A. V., Hopcroft J. E. and Ullman J. D. The Design and Analysis of Computer Algorithms. Addison-Wesley, Reading, Mass. (1974).

17. Le Metayer D., ACE: An automatic complexity evaluator. ACM Trans. Prog. Lang. Syst. 10, 248-266 (1988). 18. Katayama T. Translation of attribute grammars into procedures. ACM Trans. Prog. Lang. Syst. 6, 345-369 (1984). 19. Knuth D. Fundamental Algorithms: The Art of Computer Programming, pp. 259-265. Addison-Wesley, Reading, Mass.

(1968).

Page 10: Creating efficient programs by exchanging data for procedures

20 JOHN FRANCO and DANIEL P. FRIEDMAN

20. Griffin T. G. Notational definition--A formal account. Proc. 3rd IEEE Symposium on Logic in Computer Science, pp. 372-383 (1988).

21. Bawden A. and Rees J. Syntactic closures. Proc. 1988 ACM Symposium on LISP and Functional Programming, pp. 96-105 (1988).

22. Garey M. and Johnson D. S. Computers and Intractability: A Guide to the Theory ofNP-Completeness. W. H. Freeman, San Francisco (1979).

About the Author--JoHN VINCENT FRANCO received the B.E.E. degree from City College of New York in 1969, the M.E.E. degree from Columbia University in 1971, and the Ph.D. degree in Computer Science fromn Rutgers University in 1981. From 1969 to 1976 Dr Franco was a member of the Digital Signal Processing group at Bell Telephone Laboratories in Holmdel, New Jersey. From 1981 to 1983 he was an Assistant Professor of Computer Science at Case Western Reserve University, Cleveland, Ohio. In 1984 he joined the Computer Science Department at Indiana University, Bloomington, Indiana. His technical interests include Analysis of Algorithms, Theoretical Computer Science, Scheme, and Continuations. Dr Franco is a member of ACM, SIGACT and SIAM.

About the Author--DANIEL P. FRIEDMAN received his B,S. in Mathematics and Computer Science from the University of Houston in 1967 and his M.A, in Computer Science from the University of Texas-Austin in 1969. He completed his thesis, "GROPE: A graph processing language and its formal definition", under the direction of Terry Pratt in 1973. In 1973, after two years on the faculty of the Lyndon Baines Johnson School of Public Affairs, he joined the faculty of the Department of Computer Science at Indiana University. He was promoted to Professor in 1984. Friedman is the author of two books, The Little LISPer and Coordinated Computing, and numerous research articles. His current research interests revolve around the study of programming languages. During the past twelve years his research focused on functional programming and the use of the language Scheme as a vehicle for semantic characterizations of programming constructs. He is a member of the editorial boards of New Generation Computing and Lisp and Symbolic Computations.

Page 11: Creating efficient programs by exchanging data for procedures

Creating efficient programs by exchanging data for procedures 21

A P P E N D I X A

The Compile-Time Expansion of the Test Program for topo (let ([out-list (open-list)])

(letrec ([a (lambda ()

(set! a (err-fun 'a)) (b) (c) (e) (add-to-list 'a out-list) (set! a done-fun))]

[b (lambda () (set! b (err-fun 'b)) (d) (f) (add-to-list 'b out-list) ( s e t ! b d o n e - f u n ) ) ]

[c ( lambda () (set! c (err-fun 'c)) (b) (add-to-list 'c out-list (set! c done-fun))]

[d (lambda () (set! d (err-fun 'd)) (add-to-list 'd out-list (set! d done-fun))]

[e (lambda () (set! e (err-fun 'e)) (b) (f) (add-to-list 'e out-list) (set! e done-fun))]

[f (lambda () (set! f (err-fun 'f)) (d) (add-to-list 'f out-list) (set! f done-fun))])

(a) (b) (c) (d) (e) (f) (close-list out-list)))

Supporting Procedures for topo (define open-list

(lambda ()

(cons '() '())))

(define close-list

(lambda (out)

(car out)))

(define add-to-list

(lambda (n ~ out)

(let ([cell (cons n" '())])

(if (null? (car out))

(begin

(set-car! out cell)

(set-cdr! out cell))

(begin

(set-cdr! (cdr out) cell)

(set-cdr! out cell))))))

(define err-fun

(lambda (n)

(lambda ()

(error n "is part of a cycle"))))

(define done-fun (lambda () 'done))

Page 12: Creating efficient programs by exchanging data for procedures

22 JOHN FRANCO and DANIEL P. FRIEDMAN

A P P E N D I X B

A Lazy, Imperative Version of topo

(extend-syntax (topo) [(topo ([n (m ...)] ...)) (call/cc

(lambda (exit) (let ([out-list (open-list exit)])

(letrec (In (lambda () (set! n (err-fun 'n)) (m) ...

(add-to-list 'n out-list) ( s e t ! n done - fun ) ) ]

• . . )

(n) ... (close-list out-list)))))])

(define open-list (lambda (exit)

(let ([v (cons '() '*)]) (let ([head (cons '* v)])

(call/cc (lambda (k)

(set-cdr! v k) (exit v)))

head))))

(define close-list (lambda (out)

(add-to-list 'eos out)))

(define add-to-list (lambda (n" out)

(call/cc (lambda (k)

(let ([cell (cons '() k)]) (let* ([tail (cdr out)]

(set-car! tail n') (set-cdr! tail cell) (set-cdr! out cell) (tail-k '*)))))))

[tail-k (cdr tail)I)

Page 13: Creating efficient programs by exchanging data for procedures

Creating efficient programs by exchanging data for procedures 23

(define car" (lambda (s)

(step s) (car s)))

(define cdr ̂ (lambda (s)

(step s) (cdr s)))

define null^? (lambda (s)

(step s) (eq? (car s) 'cos)))

define step (lambda (s)

(let ([old-k (cdr s)]) (when (procedure? old-k)

(call/cc (lambda (k)

(set-cdr! s k) (old-k ' * ) ) ) ) ) ) )

define copy-s (lambda (s)

(if (null'? s) ,()

(cons (car" s) (copy-s (cdr" s))))))

define test (lambda ()

(copy-s (topo ([a (b c e)] [b (d f)] [c (b)] [d ()] [e (b f ) ] [~ ( d ) ] ) ) ) ) )