Next: , Previous: Garbage Collection, Up: General Libguile Concepts


4.3.3 Control Flow

Scheme has a more general view of program flow than C, both locally and non-locally.

Controlling the local flow of control involves things like gotos, loops, calling functions and returning from them. Non-local control flow refers to situations where the program jumps across one or more levels of function activations without using the normal call or return operations.

The primitive means of C for local control flow is the goto statement, together with if. Loops done with for, while or do could in principle be rewritten with just goto and if. In Scheme, the primitive means for local control flow is the function call (together with if). Thus, the repetition of some computation in a loop is ultimately implemented by a function that calls itself, that is, by recursion.

This approach is theoretically very powerful since it is easier to reason formally about recursion than about gotos. In C, using recursion exclusively would not be practical, though, since it would eat up the stack very quickly. In Scheme, however, it is practical: function calls that appear in a tail position do not use any additional stack space (see Tail Calls).

A function call is in a tail position when it is the last thing the calling function does. The value returned by the called function is immediately returned from the calling function. In the following example, the call to bar-1 is in a tail position, while the call to bar-2 is not. (The call to 1- in foo-2 is in a tail position, though.)

     (define (foo-1 x)
       (bar-1 (1- x)))
     
     (define (foo-2 x)
       (1- (bar-2 x)))

Thus, when you take care to recurse only in tail positions, the recursion will only use constant stack space and will be as good as a loop constructed from gotos.

Scheme offers a few syntactic abstractions (do and named let) that make writing loops slightly easier.

But only Scheme functions can call other functions in a tail position: C functions can not. This matters when you have, say, two functions that call each other recursively to form a common loop. The following (unrealistic) example shows how one might go about determing whether a non-negative integer n is even or odd.

     (define (my-even? n)
       (cond ((zero? n) #t)
             (else (my-odd? (1- n)))))
     
     (define (my-odd? n)
       (cond ((zero? n) #f)
             (else (my-even? (1- n)))))

Because the calls to my-even? and my-odd? are in tail positions, these two procedures can be applied to arbitrary large integers without overflowing the stack. (They will still take a lot of time, of course.)

However, when one or both of the two procedures would be rewritten in C, it could no longer call its companion in a tail position (since C does not have this concept). You might need to take this consideration into account when deciding which parts of your program to write in Scheme and which in C.

In addition to calling functions and returning from them, a Scheme program can also exit non-locally from a function so that the control flow returns directly to an outer level. This means that some functions might not return at all.

Even more, it is not only possible to jump to some outer level of control, a Scheme program can also jump back into the middle of a function that has already exited. This might cause some functions to return more than once.

In general, these non-local jumps are done by invoking continuations that have previously been captured using call-with-current-continuation. Guile also offers a slightly restricted set of functions, catch and throw, that can only be used for non-local exits. This restriction makes them more efficient. Error reporting (with the function error) is implemented by invoking throw, for example. The functions catch and throw belong to the topic of exceptions.

Since Scheme functions can call C functions and vice versa, C code can experience the more general control flow of Scheme as well. It is possible that a C function will not return at all, or will return more than once. While C does offer setjmp and longjmp for non-local exits, it is still an unusual thing for C code. In contrast, non-local exits are very common in Scheme, mostly to report errors.

You need to be prepared for the non-local jumps in the control flow whenever you use a function from libguile: it is best to assume that any libguile function might signal an error or run a pending signal handler (which in turn can do arbitrary things).

It is often necessary to take cleanup actions when the control leaves a function non-locally. Also, when the control returns non-locally, some setup actions might be called for. For example, the Scheme function with-output-to-port needs to modify the global state so that current-output-port returns the port passed to with-output-to-port. The global output port needs to be reset to its previous value when with-output-to-port returns normally or when it is exited non-locally. Likewise, the port needs to be set again when control enters non-locally.

Scheme code can use the dynamic-wind function to arrange for the setting and resetting of the global state. C code can use the corresponding scm_internal_dynamic_wind function, or a scm_dynwind_begin/scm_dynwind_end pair together with suitable 'dynwind actions' (see Dynamic Wind).

Instead of coping with non-local control flow, you can also prevent it by erecting a continuation barrier, See Continuation Barriers. The function scm_c_with_continuation_barrier, for example, is guaranteed to return exactly once.