Bottom-Avoiding Streams Joseph P. Near? , Ramana Kumar?? , William E. Byrd, and Daniel P. Friedman Indiana University, Bloomington, IN 47405, USA {jnear,ramkumar,webyrd,dfried}@indiana.edu Abstract. We present the first shallow embedding of ferns, stream-like bottom-avoiding, shareable, and implicitly-parallel data structures proposed in 1979. Our implementation is written as a portable library written in a small subset of R6 RS Scheme. Unlike the original conceptualization of ferns, our implementation is sequential but addresses most of the issues that would appear in a parallel implementation. We also present a non-trivial example showing the utility of ferns: a generalization of standard logic combinators. 1 Introduction We present the first shallow embedding of ferns [1], a shareable data structure designed to avoid divergence. Our implementation is written in Scheme and is available as a portable R6 RS Scheme [2] library. In addition to this implementation, we also present an extended example demonstrating the power of ferns. In this example we provide a bottom-avoiding generalization of stream-based logic combinators. In the early work of Friedman and Wise [3], the implementation of ferns assumed an arbitrary number of processors along with a proposed hardware interlock-free store instruction. We model the parallelism that inspired the ferns concept using preemptible computations on a single processor. Although the single processor model doesn’t capture all of the ramifications of the original ideas, our implementation reveals enough of the intricacies so that ferns can be easily adapted to other architectures. Ferns are constructed with cons and cons⊥ , originally called frons [4], and accessed by car ⊥ and cdr ⊥ . Ferns built with cons⊥ are like streams in that the evaluation of elements is delayed, permitting unbounded data structures. In contrast to streams, the ordering of elements is also delayed: convergent values form the prefix in some unspecified order, while divergent values form the suffix. The rest of the paper is as follows. In Section 2, we give examples of familiar recursive functions using ferns. In Section 3, we give an extended example using ferns: a generalization of logic programming combinators. In Section 4 we describe the promotion algorithm [3] that characterizes the necessary sharing properties of ferns. In Section 5, we present our complete implementation ? ?? now at MIT on exchange from The Australian National University of cons⊥ , car ⊥ , and cdr ⊥ . In Section 6 we present related work. Finally, we present our conclusions and some problems for future research. 2 Examples We begin by presenting several examples illustrating the nondeterministic properties of ferns, showing their similarities to and differences from traditional lists and streams. Later, we include examples that show that the natural recursive style can be used when programming with ferns and point out the advantages ferns afford the user. 2.1 Nondeterminism Convergent elements of a fern form its prefix in some unspecified order. For example, evaluating the expression (let ((s (cons⊥ 0 (cons⊥ 1 ’())))) (display (car ⊥ s)) (display (cadr⊥ s)) (display (car ⊥ s))) prints either 010 or 101, demonstrating that the order of values within a fern is not specified in advance but remains consistent once determined, while (let ((s 1 (cons⊥ (! 6) ⊥)) (s 2 (cons⊥ ⊥ (cons⊥ (! 5) ⊥)))) (cons (car ⊥ s 1 ) (car ⊥ s 2 ))) returns (720 . 120), demonstrating that accessing a fern avoids divergence as much as possible. (⊥ is any expression whose evaluation diverges.) In the latter example, each fern contains only one convergent value; taking the cdr ⊥ of s 1 or the cadr⊥ of s 2 results in divergence. Ferns are shareable data structures; sharing, combined with delayed ordering of values, can result in surprising behavior. For example, consider these expressions: (let ((b (cons 2 ’()))) (let ((a (cons 1 b))) (list (car a) (cadr a) (car b)))) and (let ((b (cons⊥ 2 ’()))) (let ((a (cons⊥ 1 b))) (list (car ⊥ a) (cadr⊥ a) (car ⊥ b)))) where the first expression must evaluate to (1 2 2). The second expression may also return this value—as expected, the car of b would then be equal to the cadr of a. However, the second expression might instead return (2 1 2); in this case, the car of b would be equal to the car of a rather than to its cadr. Section 4 discusses sharing in detail. 2.2 Recursion We now present examples of the use of ferns in simple recursive functions. Consider the definition of ints-from ⊥ 1 . (define ints-from ⊥ (λt (n) (cons⊥ n (ints-from ⊥ (+ n 1))))) Then (caddr⊥ (ints-from ⊥ 0)) could return any non-negative integer, whereas the streams version would return 2. There is a tight relationship between ferns and lists, since every cons pair is a fern. The empty fern is also represented by (), and (pair? (cons⊥ e 1 e 2 )) returns #t for all e 1 and e 2 . After replacing the list constructor cons with the fern constructor cons⊥ , many recursive functions operating on lists avoid divergence. For example, map⊥ is defined by replacing cons with cons⊥ , car with car ⊥ , and cdr with cdr ⊥ in the definition of map, and can map a function over an infinite fern (caddr⊥ (map⊥ add 1 (ints-from ⊥ 0))) where the result can be any positive integer. Ferns work especially well with annihilators. True values are annihilators for or ⊥ , so we can write (define or ⊥ (λt (s) (cond ((null? s) #f) ((car ⊥ s) (car ⊥ s)) (else (or ⊥ (cdr ⊥ s)))))) which searches in a fern for a true convergent value. and can avoid divergence if it finds one: (or ⊥ (list⊥ ⊥ (odd? 1) (! 5) ⊥ (odd? 0))) returns some true value, where list⊥ is defined as follows. (define-syntax list⊥ (syntax-rules () (( ) ’()) (( e e ∗ . . . ) (cons⊥ e (list⊥ e ∗ . . . ))))) We can define append⊥ for ferns. (define append⊥ (λt (s 1 s 2 ) (cond ((null? s 1 ) s 2 ) (else (cons⊥ (car ⊥ s 1 ) (append⊥ (cdr ⊥ s 1 ) s 2 )))))) 1 λt is identical to λ, except it creates preemptible procedures. (See Section 5.) To observe the behavior of append⊥ we define take⊥ (define take⊥ (λt (n s) (cond ((null? s) ’()) ((not n) (cons (car ⊥ s) (take⊥ n (cdr ⊥ s)))) ((zero? n) ’()) (else (cons (car ⊥ s) (if (= n 1) ’() (take⊥ (− n 1) (cdr ⊥ s)))))))) which returns a list of the first n values in a fern. If n = #f, take⊥ attempts to retrieve every element of the fern. When determining the nth value, it is necessary to avoid taking the cdr ⊥ after the nth value is determined. Our definition of append⊥ appears to work as expected: (take⊥ 2 (append⊥ (list⊥ 1) (list⊥ ⊥ 2))) ⇒ (1 2). Moving ⊥ from the second argument to the first, however, reveals a problem: (take⊥ 2 (append⊥ (list⊥ ⊥ 1) (list⊥ 2))) ⇒ ⊥. Even though we can see that the result of the call to append⊥ should contain two convergent elements, taking the first two elements of that result diverges. This is because our current definition of append⊥ requires that s 1 be completely exhausted before any elements from s 2 can appear in the result. If one of the elements of s 1 is ⊥, then no element from s 2 will ever appear. The same is true if s 1 contains an unbounded number of convergent elements: since s 1 is never null, the result will never contain elements from s 2 . As we will see with the definition of mplus⊥ in Section 3.1, the solution to these problems is to interleave the elements from s 1 and s 2 in the resulting fern as in the next example. Functional programs often share rather than copy data, and ferns are designed to encourage this programming style. Consider a procedure to compute the Cartesian product of two ferns: (define Cartesian-product⊥ (λt (s 1 s 2 ) (cond ((null? s 1 ) ’()) (else (mplus⊥ (map⊥ (λ (e) (cons (car ⊥ s 1 ) e)) s 2 ) (Cartesian-product⊥ (cdr ⊥ s 1 ) s 2 )))))) (take⊥ 6 (Cartesian-product⊥ (list⊥ ⊥ ’a ’b) (list⊥ ’x ⊥ ’y ⊥ ’z))) ((a . x) (a . y) (b . x) (a . z) (b . y) (b . z)) where indicates one of the possible values. This definition ensures that the resulting fern shares elements with the ferns passed as arguments. Many references to a particular element may be made without repeating computations, hence the expression (take⊥ 2 (Cartesian-product⊥ (list⊥ (begin (display #t) 5)) (list⊥ ’a ⊥ ’b))) ((5 . a) (5 . b)) prints #t exactly once. (There are more examples of the use of ferns [5–8].) 3 Extended Example: New Logic Combinators In this section, we use the task of logic programming as an extended example of the use of ferns in avoiding bottom while maintaining a natural recursive style. We compare two sets of logic combinators, one using streams and the other using ferns. We begin by describing and implementing operators mplus⊥ and bind ⊥ over ferns, and go on to implement logic programming combinators in terms of these operators. The fern-based logic combinators are shown to be more general than the standard stream-based ones. (See the historical account of logic combinators of Wand and Vaillancourt [9].) 3.1 mplus and bind To develop logic programming combinators in a call-by-value language, we must make mplus⊥ itself lazy to avoid diverging when its arguments diverge. We accomplish this by defining mplus⊥ as a macro that wraps its two arguments in list⊥ before passing them to mplus-aux ⊥ . In addition, mplus⊥ must interleave elements from both of its arguments so that a fern of unbounded length in the first argument will not cause the second argument to be ignored. (define-syntax mplus⊥ (syntax-rules () (( s 1 s 2 ) (mplus-aux ⊥ (list⊥ s 1 s 2 ))))) (define mplus-aux ⊥ (λt (p) (cond ((null? (car ⊥ p)) (cadr⊥ p)) (else (cons⊥ (caar ⊥ p) (mplus-aux ⊥ (list⊥ (cadr⊥ p) (cdar⊥ p)))))))) (define bind ⊥ (λt (s f ) (cond ((null? s) ’()) (else (mplus⊥ (f (car ⊥ s)) (bind ⊥ (cdr ⊥ s) f )))))) We use a fern constructor to make mplus⊥ lazy: if one of the ferns in the argument to mplus-aux ⊥ is divergent, it can select the other one. For example, (car ⊥ (mplus⊥ ⊥ (list⊥ 5))) returns 5. bind ⊥ avoids the same types of divergence as map⊥ described in Section 2 but uses mplus⊥ to merge the results of the calls to f . Thus, (bind ⊥ (ints-from ⊥ 0) ints-from ⊥ ) is an unbounded fern of integers; for every (nonnegative) integer n, it contains the integers starting from n and therefore every nonnegative integer n is contained n + 1 times. The interleaving leads to duplicates in the following example: (take⊥ 15 (bind ⊥ (ints-from ⊥ 0) ints-from ⊥ )) (0 1 1 2 2 3 2 4 3 5 3 6 4 7 3). The addition of unit ⊥ and mzero ⊥ rounds out the set of operators typically used to implement logic programs in functional languages. (define unit ⊥ (λt (σ) (cons σ ’()))) (define mzero ⊥ (λt () ’())) Using these definitions, we can run programs that require both nondeterminism and multiple unbounded ferns, such as this variant of Seres and Spivey [10]: (car ⊥ (bind ⊥ (ints-from⊥ 2) (λt (a) (bind ⊥ (ints-from⊥ 2) (λt (b) (if (= (∗ a b) 9) (unit ⊥ (list a b)) (mzero ⊥ ))))))) ⇒ (3 3). In this example, the streams version diverges, since 2 does not evenly divide 9. 3.2 Implementation of Logic Programming Combinators Our combinators comprise three goal constructors: ≡⊥ , which unifies terms; disj⊥ , which performs disjunction over goals; and conj⊥ , which performs conjuction over goals. These goal constructors are required to terminate, and they always return a goal. A goal is a procedure that takes a substitution and returns a fern of substitutions. (define-syntax ≡⊥ (syntax-rules () (( u v ) (λt (σ) (let ((σ (unify u v σ))) (if (not σ) (mzero ⊥ ) (unit ⊥ σ))))))) (define-syntax disj⊥ (syntax-rules () (( g 1 g 2 ) (λt (σ) (mplus⊥ (g 1 σ) (g 2 σ)))))) (define-syntax conj⊥ (syntax-rules () (( g 1 g 2 ) (λt (σ) (bind ⊥ (g 1 σ) g 2 ))))) Logic programs evaluate to goals; to obtain answers, these goals are applied to the empty substitution. The result is a fern of substitutions representing answers. We define run⊥ in terms of take⊥ , described in Section 2, to obtain a list of answers from the fern of substitutions (define run⊥ (λt (n g) (take⊥ n (g empty-σ)))) where n is a non-negative integer (or #f) and g is a goal. Given two logic variables x and y, we present some simple logic programs that produce the same answers in both fern-based and stream-based combinators. (run⊥ (run⊥ (run⊥ (run⊥ (run⊥ (run⊥ #f (≡⊥ 1 x )) ⇒ ({x/1}) 1 (conj⊥ (≡⊥ y 3) (≡⊥ x y))) ⇒ ({x/3, y/3}) 1 (disj⊥ (≡⊥ x y) (≡⊥ y 3))) ⇒ ({x/y}) 5 (disj⊥ (≡⊥ x y) (≡⊥ y 3))) ⇒ ({x/y} {y/3}) 1 (conj⊥ (≡⊥ x 5) (conj⊥ (≡⊥ x y) (≡⊥ y 4)))) ⇒ () #f (conj⊥ (≡⊥ x 5) (disj⊥ (≡⊥ x 5) (≡⊥ x 6)))) ⇒ ({x/5}) It is not difficult, however, to find examples of logic programs that diverge when using stream-based combinators but converge using fern-based combinators: (run⊥ 1 (disj⊥ ⊥ (≡⊥ x 3))) ⇒ ({x/3}) (run⊥ 1 (disj⊥ (≡⊥ ⊥ x ) (≡⊥ x 5))) ⇒ ({x/5}) and given idempotent substitutions [11], the fern-based combinators can even avoid some circularity-based divergence without the occurs-check, while streambased combinators cannot: (run⊥ 1 (disj⊥ (≡⊥ (list x ) x ) (≡⊥ x 6))) ⇒ ({x/6}) We can also write functions that represent relations. The relation alwaysfive⊥ associates 5 with its argument an unbounded number of times: (define always-five⊥ (λt (x ) (disj⊥ (always-five⊥ x ) (≡⊥ x 5)))) Because both stream and fern constructors do not evaluate their arguments, we may safely evaluate the goal (always-five⊥ x ), obtaining an unbounded collection of answers. Using run⊥ , we can ask for a finite number of these answers. Because the ordering of streams is determined at construction time, however, the stream-based combinators cannot even determine the first answer in that collection, while the fern-based combinators compute as many answers as desired: (run⊥ 4 (always-five⊥ x )) ⇒ ({x/5} {x/5} {x/5} {x/5}). This is because the definition of always-five⊥ is left recursive. In the next section we look at how the sharing properties of ferns are maintained alongside bottom-avoidance. 4 Sharing and Promotion In this section, we provide examples and a high-level description of the promotion algorithm of Friedman and Wise [3]. The values in a fern are computed and promoted across the fern while ensuring that the correct values are available from each subfern, ⊥’s are avoided, and non-⊥ values are computed only once. Ferns have structure, and there may be references to more than one subfern of a particular fern. Consider the example expression (let ((δ (cons⊥ (! 6) ’()))) (let ((γ (cons⊥ (! 3) δ))) (let ((β (cons⊥ (! 5) γ))) (let ((α (cons⊥ ⊥ β))) (list (take⊥ 3 α) (take⊥ 3 β) (take⊥ 2 γ) (take⊥ 1 δ)))))) ((6 120 720) (6 120 720) (6 720) (720)) assuming list evaluates its arguments left-to-right. Since the subfern δ is itself a fern, accessing δ cannot retrieve values in the prefix of the enclosing fern α. We now describe in detail how the result of (take⊥ 3 α) is determined, and the necessary changes to the fern data structure during this process. Whenever we encounter a nondeterministic choice, we shall assume a choice consistent with the value returned in the example. During the first access of α the cdrs are evaluated, as indicated by the arrows in Figure 1a. Figure 1b depicts the data structure after (car ⊥ α) is evaluated. We assume that, of the possible values for (car ⊥ α), namely ⊥ (which is never chosen), (! 5), (! 3), and (! 6), the value of (! 3) is chosen and promoted. Since the value of (! 3) might be a value for (car ⊥ β) and (car ⊥ γ), we replace the cars of all three pairs with the value of (! 3), which is 6. We replace the cdrs of α and β with new frons pairs containing ⊥ and (! 5), which were not chosen. The new frons pairs are linked together, and linked at the end to the old cdr of γ. Thus α, β, and γ each become a fern with 6 in the car and a fern of the rest of their original possible values in their cdrs. As a result of the promotion, α, β, and γ become cons pairs, represented in the figures by rectangles. Figure 1c depicts the data structure after (cadr⊥ α) is evaluated. This time, (! 5) is chosen from ⊥, (! 5), and (! 6). Since the value of (! 5) is also a possible value for (cadr⊥ β), we replace the cadrs of both α and β with the value of (! 5), which is 120, and replace the cddr of α with a frons pair containing the ⊥ that was not chosen. The cddr of β points to δ; no new fern with remaining possible values is needed because the value chosen for (cadr⊥ β) was the first value available. As before, the pairs containing values become cons pairs. Figure 1d depicts the data structure after (caddr⊥ α) is evaluated. Of ⊥ and (! 6), it comes as no surprise that (! 6) is chosen. Since the value of (! 6), which is 720, is also a possible value for (car ⊥ δ) (and in fact the only one), we update the car of δ and the car of the cddr of α with 720. The cdr of δ remains as the empty list, and the cdr of the cddr of α becomes a new frons pair containing ⊥. The cdr of the new frons pair is the empty list copied from the cdr of δ. The remaining values are obvious given the final state of the data structure. No further manipulation of the data structure is necessary to evaluate the three remaining calls to take⊥ . In Figure 1d we see that each of the ferns α, β, γ, and δ contains some permutation of its original possible values, and ⊥ has been pushed to the end of α. Furthermore, if there are no shared references to β, γ, and δ, the number of accessible pairs is linear in the length of the fern. If there are references to subferns, for a fern of size n, the worst case is (n2 + n)/2. But, as these shared references vanish, so do the additional cons pairs. α γ β α δ γ β 6 6 ⊥ !6 !3 !5 !6 ⊥ (a) α γ β 6 6 120 120 δ 6 α δ 6 !6 !5 (b) γ β 6 6 120 120 δ 6 720 720 ⊥ (c) ⊥ (d) Figure 1. Fern α immediately after evaluation of cdrs, but before any cars have finished evaluation (a) and after the values, 6 (b), 120 (c), and 720 (d) have been promoted. If list evaluated from right-to-left instead of evaluating from left-to-right, the example expression would return ((720 6 120) (720 6 120) (720 6) (720)). Each list would be independent of one another and the last pair of α would be a frons pair with ⊥ in the car and the empty list in the cdr. This demonstrates that if there is sharing of these lists, the lists contain four pairs, three pairs, two pairs, and one pair, respectively. If the example expression just returned α, then only four pairs would be accessible. The example presented in this section provides a direct view of promotion. When a fern is accessed by multiple computations, the promotion algorithm must be able to handle various issues such as multiple values becoming available for promotion at once. The code presented in the next section handles these details. 5 Ferns Implementation In this section we present a complete, portable, R6 RS compliant implementation of ferns2 . We begin with a description of engines [12], which we use to handle suspended, preemptible computations. We then describe and implement frons pairs, the building blocks of ferns. Next we present car ⊥ and cdr ⊥ which work on both frons pairs and cons pairs. Taking the car ⊥ of a frons pair involves nondeterministically choosing one of the possible values in the fern and promoting the chosen value. Taking the cdr ⊥ of a frons pair ensures the first value in the pair is determined and returns the second value (usually the rest of the fern). Taking the car ⊥ (cdr ⊥ ) of a cons pair is the same as taking its car (cdr ). 2 Our ferns library is available at http://www.cs.indiana.edu/∼webyrd/ferns.html 5.1 Engines An engine is a procedure that computes a delayed value in steps. To demonstrate the use of engines, consider the procedure (define wait (λt (n) (cond ((zero? n) #t) (else (wait (− n 1)))))) To create an engine e to delay a call to (wait 20), we write: (define e (engine (wait 20))) To partially compute (wait 20), we call e with a number of ticks and two handler procedures: (e 5 (λ (t v ) (list t v )) (λ (ê) #f)) ⇒ #f. This call runs the expression (wait 20) with 5 ticks. In our implementation, a tick is spent on each call to a procedure defined with λt . If the expression is not completely evaluated after all the ticks are spent, the second handler is called with a new engine containing the rest of the computation. In this case, we discard the new engine and return #f. We could, however, call the new engine to complete the computation. If the computation finishes, the first handler is called with the unspent ticks and the computed value. For example, consider (let ((succeed (λ (t v ) (list t v )))) (letrec ((fail (λ (ê) (cons #f (ê 5 succeed fail ))))) (e 5 succeed fail ))) ⇒ (#f #f #f #f 4 #t). In this example, (wait 20) calls wait a total of 21 times (including the initial call), so 4 ticks are unspent after the last call to ê. The delayed computation in an engine may involve creating and calling more engines. When a nested engine [13] consumes a tick, every enclosing engine also consumes a tick. To see this, we can define the amb operator [14] using engines: (define-syntax amb (syntax-rules () (( exp 1 exp 2 ) (amb-aux (engine exp 1 ) (engine exp 2 ))))) (define amb-aux (λt (e 1 e 2 ) (e 1 1 (λ ( v ) v ) (λ (e 1 ) (amb-aux e 2 e 1 ))))) Nested calls to amb, for example (amb v 1 (amb v 2 v 3 )), rely on nestable engines. This implementation of amb is fair because our implementation of nested engines is fair: every tick given to the second engine in the outer call to amb-aux is passed on to exactly one of the engines, alternating between the engines for v 2 and v 3 , in the inner call to amb-aux . 5.2 The Ferns Data Type We represent a frons pair by a cons pair that contains at least one tagged engine (te). Engines are tagged with either L when locked (being advanced by another computation) or U when unlocked (runnable). We distinguish between locked and unlocked engines because the car ⊥ of a fern may be requested more than once simultaneously, for example in determining the car ⊥ of a fern whose values both depend on the car ⊥ of another fern: (car ⊥ (list⊥ (car ⊥ s) (cdar⊥ s))). To manage effects, we prevent the same engine from being advanced in more than one computation. We define simple predicates La? , Ua? , Ld? , and Ud? for testing whether one side of a frons pair contains a locked or unlocked engine. (define engine-tag-compare (λ (get-te tag) (λ (q) (and (pair? q) (pair? (get-te q)) (eq? (car (get-te q)) tag))))) (define (define (define (define La? Ua? Ld? Ud? (engine-tag-compare (engine-tag-compare (engine-tag-compare (engine-tag-compare car car cdr cdr ’L)) ’U)) ’L)) ’U)) The procedure step d (step a ) takes a frons pair with an unlocked tagged engine in the cdr (car) and locks and advances the tagged engine by nsteps ticks. If the engine does not finish, the tagged engine is unlocked and updated with the advanced engine. If the engine finishes with value v , then v becomes the frons pair’s cdr (car). In addition, the tagged engine will be updated with an unlocked dummy engine that returns v . We do this because the cdrs of multiple frons pairs may share a single engine, as will be explained at the end of this section. Although the cars of frons pairs never share engines, we do the same for the cars. (define step (λ (get-te set-val! ) (λ (q) (let∗ ((te (get-te q)) (set-te! (λ (e) (set-car! te ’U) (set-cdr! te e)))) (set-car! te ’L) (let ((e (cdr te))) (e nsteps (λ ( v ) (set-val! q v ) (set-te! (engine v ))) set-te! )))))) (define step a (step car set-car! )) (define step d (step cdr set-cdr! )) Now we present the implementation of the fern operators. 5.3 cons⊥ , car ⊥ , and cdr ⊥ cons⊥ constructs a frons pair by placing unlocked engines of its unevaluated operands in a cons pair. (define-syntax cons⊥ (syntax-rules () (( a d ) (cons (cons ’U (engine a)) (cons ’U (engine d )))))) When the car ⊥ (definition below) of a fern is requested, parallel evaluation of the possible values is accomplished by a round-robin race of the engines in the fern. During its turn, each engine is advanced a fixed, arbitrary number of ticks until a value is produced. The race is accomplished by two mutually recursive functions: race a , which works on the possible values of the fern, and race d , which moves onto the next frons pair by either following the cdr of the current frons pair or starting again at the beginning. race a dispatches on the current pair or value q. When the car of q is a locked engine, race a waits for it to become unlocked by waiting nsteps ticks and then calling race d . The call to wait is required to allow race a to be preempted at this point, so the owner of the lock does not starve. When the car is an unlocked engine, race a advances the unlocked engine nsteps ticks, then continues the race by calling race d . When q is not a pair, race a simply starts the race again from the beginning. This happens when racing over a finite fern and emerges from the else clause of race d . When the car contains a value, that value is promoted to the front of all the subferns in the chain from p to q. (define car ⊥ (λ (p) (letrec ((race a (λ (q) (cond ((La? q) (wait nsteps) (race d q)) ((Ua? q) (step a q) (race d q)) ((not (pair? q)) (race a p)) (else (promote p q) (car p))))) (race d (λ (q) (cond ((Ld? q) (race a p)) ((Ud? q) (step d q) (race a p)) (else (race a (cdr q))))))) (race a p)))) One subtlety of race a is that it does not care, after advancing an engine nsteps ticks, whether that engine completed. The value will be picked up the next time the race comes around, if necessary. Calling promote immediately would be incorrect because an engine may be preempted while advancing, at which point promotion from p may be performed by another computation with a different value for the car of p. race d also dispatches on q, this time examining its cdr. When the cdr of q is a locked engine, race d , being unable to proceed further down the fern, restarts the race by calling race a on p. When the cdr of q contains an unlocked engine, race d advances the engine nsteps ticks as in race a , and then restarts the race. If that engine finishes with a new frons pair, the new pair will then be competing in the race and will be examined next time around. When the cdr of q is a value, usually a fern, race d continues the race by passing it to race a ; if a non-pair value is at the end of a fern it will be picked up by the third clause in race a . car ⊥ avoids starvation by running each engine in a car for the same number of ticks. During a race, a subfern of the fern in question is in a fair state: up to a point, there are no engines in the cdrs, so each potential value in a car is considered equally. When this fair subfern is not the entire fern, the race devotes the same number of ticks to lengthening the fair subfern as it does to each element of that subfern. Since cdr engines often evaluate to pairs quickly, the entire fern usually becomes fair in a number of races equal to the length of the fern. When cdr engines do not finish quickly, however, the process of making the entire fern fair can take much longer, especially for long ferns. The cost of finding the value of an element occurring near the end of such a fern can be much greater than the cost for an element near the beginning. promote (definition below) propagates a convergent value found in the pair q across the subfern from p, whose car was requested, to q. Each frons pair in this chain is transformed into a cons pair whose car is the convergent value and whose cdr is a copy of that frons pair. These new frons pairs are connected as a fern and terminate at the cdr of q. After promotion, the subferns from p to the cdr of q contain the convergent value in their cars and a fern containing the remaining possible values in their cdrs. (define promote (λ (p q) (cond ((eq? p q) (cdr q)) (else (let ((new-p (cons (car p) (promote (cdr p) q)))) (set-car! p (car q)) (set-cdr! p new-p) new-p))))) The cdr of a fern (definition below) cannot be determined until the fern’s car has been determined. Once the car has been determined, there is no longer parallel competition between potential cdrs. Thus, we can use cdrs , which takes the cdr of a stream. Then, since q’s car has been determined, q has therefore become a cons pair, so cdr ⊥ returns the value in q’s cdr. (cars ’s definition follows by replacing all d s by as. conss is the same as cons⊥ , and the definitions of the other stream operators: mpluss , mplus-aux s , bind s , disjs , and conjs follow the definitions of Section 3 with operators f⊥ replaced by fs .) (define cdr ⊥ (λ (q) (car ⊥ q) (cdrs q))) (define cdrs (λ (q) (cond ((Ld? q) (wait nsteps) (cdrs q)) ((Ud? q) (step d q) (cdrs q)) (else (cdr q))))) If the engine being advanced by cdr ⊥ completes, cdr ⊥ indicates that step d should replace the tagged engine in p by the computed value. The value will be picked up after loop terminates. cdr ⊥ also indicates that the pair representing the tagged engine should be unlocked and the engine should be replaced with a new engine that simply returns the value. race d and cdr ⊥ are required to not only update the frons pair with the calculated value, but also they must update the tagged engine because there might be a fern other than p sharing this engine. Consider the following expression where we assume list evaluates its arguments from left to right. (let ((β (cons⊥ 1 (ints-from ⊥ 2)))) (let ((α (cons⊥ ⊥ β))) (list (car ⊥ α) (cadr⊥ β) (cadr⊥ α)))) (1 2 2) Figure 2 below shows the data structures involved in evaluating the expression. Figure 2a shows α immediately after it has been constructed, with engines delaying evaluation of ⊥ and β. In evaluating (car ⊥ α), the engine for β finishes, resulting in Figure 2b. β can now participate in the race for (car ⊥ α). Suppose the value 1 found in the car of β is chosen and promoted from α to β. The result is Figure 2c, in which the engine delaying (ints-from ⊥ 2) is shared by both β and the cdr of α. (cadr⊥ β) forces calculation of (ints-from ⊥ 2), which results in a fern, γ, whose first value (in this example) is 2. Figure 2d now shows why step d updates the current pair (β) and creates a new engine with the calculated value (γ): the cddr of α needs the new engine to avoid recalculation of (ints-from ⊥ 2). In Figure 2e when (cadr⊥ α) is evaluated, the value 2, calculated already by (cadr⊥ β), is promoted and the engine delaying (ints-from ⊥ 3) is shared by both α and β. α α β α 1 β ⊥ γ β 1 (c) ⊥ α 2 γ β 1 ι3 ⊥ ι2 (b) 1 1 ι2 ⊥ (a) α β 1 1 2 ι3 2 γ (d) (e) ⊥ Figure 2. Fern α after construction (a); after β in the cdr of α has been evaluated (b); after 1 from the car of β has been promoted to the car of α, resulting in a shared tagged engine (c); after the shared engine is run, while evaluating (cadr⊥ β), to produce a fern α (d); after 2 from the car of γ has been promoted to the cadr of α (e). 6 Related Work Previous implementations of ferns have been for a call-by-need language. The work of Friedman and Wise [3, 4, 1] differs from our shallow embedding in two major respects. First, the languages were implemented using an interpreter. Second, the laziness of the cons operator resulted in the operands to cons competing in the race described in Section 5.3. Johnson’s master’s thesis [15] under Friedman’s direction presents an interpreter implemented in Pascal for a lazy ferns language. Subsequently, Johnson and his doctoral student Jeschke implemented a series of native C symbolic multiprocessing systems based on the Friedman and Wise model. This series culminated with the parallel implementation Jeschke describes in his dissertation [8]. In their Daisy language, ferns are the means of expressing explicit concurrency [5, 6]. This work differs from our shallow embedding approach in the same ways as the earlier work of Friedman and Wise. 7 Conclusion We have presented a shallow embedding of ferns, which are shareable, bottomavoiding data structures. Ferns are useful in avoiding divergence; elements that do not converge are avoided until it is no longer possible to do so. Ferns are shareable, meaning that many different computations can use the elements of a fern without repeating computation. Since the fern constructor cons⊥ is similar to the constructor cons, writing bottom-avoiding functions is often intuitive. We have also presented some motivating examples for ferns, including a generalization of logic programming combinators. We have shown that fern-based combinators avoid more types of divergence than stream-based combinators. Ferns were originally conceptualized as a data structure for the abstraction of multiprogramming, and were specified by a formal characterization rather than a concrete implementation. Future research includes proving the correctness and fairness of our implementation, proving completeness of the fern-based combinators, and implementing a multicore shallow embedding of ferns. We hope that the relative simplicity of our work along with the ease of defining bottom-avoiding recursive functions will encourage others to take up some of these challenges. 8 Acknowledgements Guy Steele Jr.’s inspiring keynote address at Dan Friedman’s 60th birthday celebration renewed our interest in ferns. In addition, Guy made a critical observation that simplified promote, which in turn allowed us to simplify car ⊥ and cdr ⊥ . We thank Kevin Millikin, Olivier Danvy, Mitch Wand, Steve Johnson, Chung-chieh Shan, and Oleg Kiselyov for their comments on drafts of this paper. Oleg’s clever use of thunks to represent “incomplete” computations in miniKanren led to our interest in avoiding divergence whenever possible. Once again, we have found Dorai Sitaram’s excellent SLATEX package invaluable for typesetting our code. References 1. Friedman, D.P., Wise, D.S.: Fancy ferns require little care. In Holmström, S., Nordström, B., Wikström, Å., eds.: Symposium on Functional Languages and Computer Architecture, Göteborg, Sweden, Laboratory for Programming Methodology, University of Göteborg and Chalmers University of Technology (June 1981) 124–156 2. Sperber, M., Dybvig, R.K., Flatt, M., van Straaten, A. (eds.): Revised6 report on the algorithmic language Scheme (September 2007) 3. Friedman, D.P., Wise, D.S.: An approach to fair applicative multiprogramming. In Kahn, G., ed.: Semantics of Concurrent Computation: Proceedings of the International Symposium. Volume 70 of Lecture Notes in Computer Science (LNCS)., Evian, France, Springer-Verlag (Berlin/Heidelberg/New York) (July 1979) 203–225 4. Friedman, D.P., Wise, D.S.: An indeterminate constructor for applicative programming. In: Conference Record of the Seventh ACM Symposium on Principles of Programming Languages (POPL ’80), New York, USA, ACM Press (January 1980) 245–250 5. Johnson, S.D.: Circuits and systems: Implementing communication with streams. IMACS Transactions on Scientific Computation, Vol. II (1983) 311–319 6. Johnson, S.D., Jeschke, E.: Modeling with streams in Daisy/The SchemEngine Project. In Sheeran, M., Melham, T., eds.: Designing Correct Circuits, (DCC’02), ETAPS 2002 (2002) Presentation at the Workshop on Designing Correct Circuits, held on 6–7 April 2002 in Grenoble, France. 7. Filman, R.E., Friedman, D.P.: Coordinated Computing: Tools and Techniques for Distributed Software. McGraw-Hill (1984) 8. Jeschke, E.R.: An Architecture for Parallel Symbolic Processing Based on Suspending Construction. PhD thesis, Indiana University Computer Science Department (May 1995) Technical Report No. 445, 152 pages. 9. Wand, M., Vaillancourt, D.: Relating models of backtracking. In: Proc. 9th Int. Conf. on Functional Programming, ACM Press (2004) 54–65 10. Spivey, M., Seres, S.: Combinators for logic programming. The Fun of Programming (2003) 177–200 11. Lloyd, J.W.: Foundations of logic programming. Second extended edn. SpringerVerlag, New York (1987) 12. Haynes, C.T., Friedman, D.P.: Abstracting timed preemption with engines. Journal of Computer Languages 12(2) (1987) 109–121 13. Hieb, R., Dybvig, K., Anderson, III, C.W.: Subcontinuations. Lisp and Symbolic Computation 7(1) (1994) 83–110 14. McCarthy, J.: A basis for a mathematical theory of computation. In Braffort, P., Hirschberg, D., eds.: Computer Programming and Formal Systems, North Holland (1963) 33–69 15. Johnson, S.D.: An interpretive model for a language based on suspended construction. Master’s thesis, Indiana University Computer Science Department (1977) Indiana University Computer Science Department Technical report No. 68. 16. Dybvig, R.K., Hieb, R.: Engines from continuations. Comput. Lang 14(2) (1989) 109–123 17. Baader, F., Snyder, W.: Unification theory. In Robinson, A., Voronkov, A., eds.: Handbook of Automated Reasoning. Volume I. Elsevier Science (2001) 445–532 A Nestable Engines Our implementation of ferns requires nestable engines [16, 13], which we present here with minimal comment. We use two global variables: timer , which holds the number of ticks available to the currently running engine and uses #f to represent infinity, and throw , which holds an escape continuation. make-engine makes an engine out of a thunk. engine is a macro that makes an engine from expressions. λt is like λ except that it passes its body, as a thunk, to expend-tickto-call , which ensures a tick is spent before the body is evaluated and passes the suspended body to throw if no ticks are available. (define timer #f) (define throw 0) (define make-engine (λ (thunk ) (λ (ticks sk fk ) (let∗ ((gift (or (and timer (min timer ticks)) ticks)) (saved-timer (and timer (− timer gift))) (saved-throw throw ) (caught (call-with-current-continuation (λ (k ) (set! timer gift) (set! throw k ) (let ((result (thunk ))) (throw (cons timer result))))))) (set! timer saved-timer ) (set! throw saved-throw ) (let ((owed (− ticks gift))) (cond ((pair? caught) (and timer (set! timer (+ timer (car caught)))) (sk (+ (car caught) owed ) (cdr caught))) (else (let ((e (make-engine caught))) (if (zero? owed ) (fk e) (let ((th (λ () (e owed sk fk )))) ((call-with-current-continuation (λ (k̂) (throw (λ () (k̂ th)))))))))))))))) (define-syntax engine (syntax-rules () (( e) (make-engine (λ () e))))) (define-syntax λt (syntax-rules () (( formals b0 b . . . ) (λ formals (expend-tick-to-call (λ () b0 b . . . )))))) (define expend-tick-to-call (λ (thunk ) ((call-with-current-continuation (λ (k ) (let th () (case timer ((#f) (k thunk )) ((1) (throw (λ () (k thunk )))) ((0) (throw th)) (else (set! timer (− timer 1)) (k thunk ))))))))) B Logic Programming Auxiliaries To complete the implementation of the logic programming combinators presented in Section 3 we provide a logic variable constructor make-var , a unification algorithm unify, and substitution helpers empty-σ, ext-σ, and walk . We represent logic variables by R6 RS [2] records (syntactic layer); defining the record type var creates the constructor make-var automatically. We represent substitutions as association lists, and use the triangular substitution model [17]. (define-record-type var ) (define unify (λ (t1 t2 σ) (let ((t1 (walk t1 σ)) (t2 (walk t2 σ))) (cond ((eq? t1 t2 ) σ) ((var? t1 ) (ext-σ t1 t2 σ)) ((var? t2 ) (ext-σ t2 t1 σ)) ((and (pair? t1 ) (pair? t2 )) (let ((σ (unify (car t1 ) (car t2 ) σ))) (and σ (unify (cdr t1 ) (cdr t2 ) σ)))) (else (if (equal? t1 t2 ) σ #f)))))) (define empty-σ ’()) (define ext-σ (λ (x t σ) ‘((,x . ,t) . ,σ))) (define walk (λ (t σ) (let ((b (assq t σ))) (if b (walk (cdr b) σ) t))))