Continuations and advanced flow control

Learn new techniques from specialized languages

Flow control is usually straightforward: sequence, selection, iteration. And many programmers, having been raised on these primary control structures, have a difficult time seeing what other kinds of flow control might be necessary. This article introduces continuations and teaches you to think about flow control in radically new ways.

Share:

Jonathan Bartlett (johnnyb@eskimo.com), Director of Technology, New Medio

Jonathan Bartlett is the author of the book Programming from the Ground Up, an introduction to programming using Linux assembly language. He is the lead developer at New Medio, developing Web, video, kiosk, and desktop applications for clients.



24 May 2006

Also available in Russian

After their first introduction to programming, many programmers don't think about flow control, since most programming languages have only a few simple flow control constructs. However, the world of flow control is much richer than most mainstream programming languages let on. Many lesser-known, specialized languages have advanced and interesting flow control constructs.

Advanced flow control examples

The best way to get started is by looking at examples of advanced flow control constructs in different languages. Then we can generalize that knowledge into a framework for advanced flow control situations.

Non-local exits

The first advanced flow control technique is one that you probably already know: non-local exits. There are several types of non-local exits, each of which can be divided into two categories, structured and unstructured. Unstructured non-local exits are those things that your computer science teacher warned you to never do, like the dreaded goto. The truth is, if used wisely and judiciously, unstructured non-local exits can be very useful. For example, they can improve the readability of programs with complicated flow control. If complex flow control is not naturally nested, imposing a nested structure on it can make it less readable, not more. For more information on the pros and cons of using gotos, see the links in the Resources section later in this article.

As for structured non-local exits, you are probably familiar with the most popular type: exceptions. If you have been programming C, Fortran, and Cobol for the last 20 years without once coming up for air, here's a brief introduction to exceptions.

An exception is a way of signalling and localizing an error within code. Usually, when an error occurs, you want the error to be handled and the rest of the work to not continue unless you give the explicit go-ahead. As an example, take a look at this simple database code in the Java™ language:

Listing 1. Simple database code example
//NOTE -- this code will not compile, but that's on purpose.
import java.sql.*;
  ...
  public static int testdbcode(Connection conn) {
    //Prepare the statement
    PreparedStatement s = conn.prepareStatement(
       "select id from test_table where name = ?");
    //Set the parameters
    s.setString(1, "fred");
    //Run the query
    ResultSet rs = s.executeQuery()
    //Move to the first result row
    rs.next();
    //Get the answer (first result parameter)
    int id = rs.getInt(1);

    return id;
  }
  ...

The problem with this code is that there is no error handling, and when dealing with databases or any other external entity, errors can happen at nearly every point. And, if you get an error anywhere within the code, it doesn't make much sense to continue on. For example, if there is an error preparing my query, there isn't much use setting parameters, running the query, or retrieving the answer. The query is pretty much toast after the first error. Therefore, in Java, they have exceptions that allow you to enclose a block of code, so that the whole block of code is skipped on the first error. To do this in the Java language, you would rewrite the code as follows:

Listing 2. Simple database function with exception handling
import java.sql.*;
  ...
  public static int testdbcode(Connection conn) {
    try {
      //Prepare the statement
      PreparedStatement s = conn.prepareStatement(
         "select id from test_table where name = ?");
      //Set the parameters
      s.setString(1, "fred");
      //Run the query
      ResultSet rs = s.executeQuery()
      //Move to the first result row
      rs.next();
      //Get the answer (first result parameter)
      int id = rs.getInt(1);

      return id;
    } catch (Exception e) {
      //Put error handling code here
      return -1;
    }
  }
  ...

The code within the try block executes until it finishes or until one statement results in an error condition. If an error is thrown, the rest of the code in the try block is skipped, and the catch block is executed, with the variable e holding the exception information. In Java, the errors that are thrown are full classes themselves, so any amount of information can be placed into the exception. In fact, multiple catch blocks can be created, each of which handle a specific exception class.

In the code example above, we dealt with system-generated exceptions. However, application-level exceptions can be created and handled just the same. The application can signal an exception at any time using the throw keyword. For example, let's say in the code above that if we get an ID number of 0, it is an application-level exception. We could do the following:

Listing 3. Simple database example with application-level exception throwing
import java.sql.*;
  ...
  public static int testdbcode(Connection conn) {
    try {
      //Prepare the statement
      PreparedStatement s = conn.prepareStatement(
         "select id from test_table where name = ?");
      //Set the parameters
      s.setString(1, "fred");
      //Run the query
      ResultSet rs = s.executeQuery()
      //Move to the first result row
      rs.next();
      //Get the answer (first result parameter)
      int id = rs.getInt(1);

      //Check for application exception
      if(id == 0) {
        //Signal the exception
        //Assume that InvalidUserIDException is defined elsewhere
        throw new InvalidUserIDException();
      }

      return id;
    } catch (InvalidUserIDException e) {
      //Specialized error handling here
      return -1;
    } catch (Exception e) {
      //Handle all other errors here
      return -1;
    }
  }
  ...

In addition, there is no reason that the code that handles the exception has to reside in the function itself. The try and catch statements can be in any containing function as well. The exception-handling mechanism will unwind the stack until it reaches an appropriate exception handler, and at that point run the exception-handling code. This ability to perform structured non-local exits can make writing code, as well as its maintenance, a lot easier, as the error handling can in some cases be wholly separated from the actual code doing the work. Of course, it is almost deceptively easy. Exception handling has several major gotchas that are outside of the scope of this article, but some of the Resources can give you some guidance on these.

Generating functions

Another type of advanced flow control structure is a generator. Generators are functions that can generate a set of values, and return them one at a time as the function is called. A generator is able to "bookmark" its place in the current function to make programming easier.

For example, let's say you want a function that returns a sequence of numbers starting with 1 and continually increasing every time you call it. While this wouldn't be hard to do with closures or even a global variable, you can also do it with generators very easily. Here is an example with Python's generator implementation:

Listing 4. Simple Python generator program
#This is our generator function
def sequence(start):
  current = start
  while true:
    yield current
    current = current + 1

generator_func = sequence(1)  #Create a new sequence generator starting at 1
print generator_func.next()   #prints 1
print generator_func.next()   #prints 2
print generator_func.next()   #prints 3

As you can see, the generator goes back to where it left off in the function each time, and then continues on until it hits a yield statement. This sort of "bookmarking" and "resume from bookmark" feature is not standard in most languages, but they are very useful, and can make complex logic much more readable and easier to program.

Logic programming

Another type of advanced flow control is logic programming, which is heavily used by programming languages such as Prolog. In Prolog, you give the computer a set of definitions and it "magically" answers queries and sets values for you. For example, look at the following Prolog program (capital letters signify variables):

Listing 5. Simple logic program in Prolog
likes(john,sports).            %this asserts that john likes sports
likes(john,food).              %this asserts that john likes food
likes(amy,X) :- likes(john,X). %amy likes X if john likes X
likes(brad,food).              %brad likes food
likes(brad,television).        %brad likes television

%query for a value that both amy and brad like, and write it out.
?- likes(amy,X), likes(brad,X), write(X).

The way this works is that Prolog will create lists of what john and brad like. It also says that the rule for what amy likes is anything that john likes. Then, when you execute the query, it first finds an answer to the question "what does amy like?" The answer to this question is "anything that john likes." It then goes through john's list of things that he likes and pulls out the first item, which is sports. It then goes to the next proposition, which is that brad must like the same thing that amy liked (denoted as X in this expression). However, sports isn't in brad's list of items. Therefore, Prolog then backtracks and finds a new value for X from amy's list. The next value is food. It then goes and checks to see if food is in brad's list. It is, so it goes on to the next step, which is to write out what value was found for X.

This sort of programming is called logic programming, because it allows you to state goals in terms of logical relationships and lets the computer do all of the legwork to find appropriate solutions to these logical relationships. The most important part of it, for our purposes, is the unique flow control that this sets up: backtracking. This means that at any point in the computation, if an appropriate value is not found in a particular variable, or a particular relationship among a group of variables is incorrect, the program can "back up" and assign a new value. This sort of programming simplifies a huge number of problem sets, especially in the area of smart systems.


Continuations: The flow control utility knife

So far, we have looked at three types of advanced flow control: non-local exits, generating functions, and backtracking. What do they have in common? Basically, each does a certain amount of gymnastics with the program stack and the instruction pointer. With non-local exits, the stack frame containing the exit blocks is bookmarked, as is the exit handling block. When you invoke the non-local exit, the stack is unwound to the bookmarked point, and the instruction pointer is moved to the handler block. With generating functions, the variable containing the generator contains a pointer to the stack frame of the generator function as well as to the point in the function where the generator last left off. In backtracking, a bookmark is kept where the assignment took place, and the flow control is reverted to that location if a computation fails and backtracking is necessary.

You could call these bookmarks "continue points" -- the place where the program will continue to if the advanced flow control structure is invoked. Or, more precisely, they are known as continuations. In fact, all of these control structures can be implemented using a single flow control function: call-with-current-continuation.

call-with-current-continuation is a function in the Scheme programming language that takes the current stack frame and instruction pointer and packages it up into a single callable entity (the continuation) and calls another function with the continuation as its only parameter. The continuation is a callable entity that takes one parameter, and that parameter is then returned to the point where the continuation is created. That may sound confusing -- and it is. Let's look at a few examples to get a feel for how it works in practice.

First, here's a quick example of using a continuation:

Listing 6. Continuation example
(display
  ;;Calling the continuation will return the parameter as the return value to
  ;;the call-with-current-continuation function.  This is the "bookmarked" location
  (call-with-current-continuation
    ;;Continuation variables are often written with a "k" or "kont" to remind
    ;;yourself that it is a continuation.  In this example, "kont" will be
    ;;the continuation.  It is a function, that, when called, will return its
    ;;parameter to the bookmarked location.
    (lambda (kont)
       ;;For this example, we will simply run the continuation.  This returns
       ;;the number 5 to the "bookmarked" location
       (kont 5))))
(newline)

The preceding program simply displays the number 5. Note that calling the continuation with a single parameter returns that value to the bookmarked location. Now, let's look at a slightly more complex example. In this case, you are going to use the continuation as an early exit. This example is contrived, but it illustrates the point. For this program, you will square every number in a list. However, if there are any non-numbers in the list, instead of returning a list, it will simply return the symbol *error*.

Listing 7. Early exit from error conditions with a continuation
(define a '(1 2 3 4 5))
(define b '(1 b 2 e f))
(define (square-list the-list)
  ;;Bookmark here
  (call-with-current-continuation
    ;;early-exit-k will return us to our bookmark
    (lambda (early-exit-k)
      ;;run the procedure
      (map
        (lambda (x)
          ;;Check to see if it is a number
          (if (number? x)
              ;;yes it is, perform the multiplication
              (* x x)
              ;;no it isn't, exit _now_ with the value *error*
              (early-exit-k '*error*)))
        the-list))))
;;This will square the list
(display (square-list a))
(newline)
;;This will give the error
(display (square-list b))
(newline)

Hopefully, the above example gives you a hint as to how you can use continuations to implement exceptions.

The next example demonstrates another unusual property of continuations, the fact that they have unlimited extent. This means that, unlike exceptions, continuations can be activated outside of their originating code block. When you make a "bookmark" with a continuation, it forces that stack frame to remain alive for as long as the continuation value is alive. Therefore, even if you return from the block that created the continuation, if you return it, calling the continuation will restore the previously active stack frame and continue on from there.

The following example saves a continuation to a global variable and then, upon activation of the continuation, reactivates the original stack frame.

Listing 8. Reactivating a stack frame with continuations
;;Global variable for the continuation
(define k-global #f)

;;We are using let* instead of let so that we can guarantee
;;the evaluation order
(let* (
       (my-number 3)
       (k
         ;;set bookmark here
         (call-with-current-continuation
           ;;The continuation will be stored in "kont"
           (lambda (kont)
             ;;return the continuation
             kont))))

     ;;We are using "my-number" to show that the old stack
     ;;frame is being saved.  When we revisit the continuation
     ;;the second time, the value will remain changed.
     (display "The value of my-number is: ")
     (display my-number)
     (newline)
     (set! my-number (+ my-number 1))

     ;;Save the continuation in a global variable.
     (set! k-global k))

;;Is "kontinuation" a continuation?
(if (procedure? k-global)
    (begin
       (display "This is the first run, k-global is a continuation")
       (newline)
       ;;Send "4" to the continuation which goes to the bookmark
       ;;which will assign it to "k"
       (k-global 4))
     (begin
       ;;This occurs on the second run
       (display "This is the second run, k-global is not a continuation, it is ")
       (display k-global)
       (newline)))

With these features, you can create interesting procedures and macros that emulate all sorts of other features.

Exceptions with continuations

Take a look at what exceptions look like:

Listing 9. Exception example
try {
     //Code here which might generate an exception
} catch(SQLException e) { //catch a specific exception
     //Error handling code
} catch(Exception e) { //catch a general exception
     //Error handling code
}

//remaining code here

So, what you basically need to do is create a macro that establishes:

  • The error-handling code block
  • The location of the remaining code
  • The code that is run

So, the result of the macro expansion needs to look something like this:

Listing 10. The desired expansion of a hypothetical exception macro
;;This establishes the throw function as globally available, but displays an
;;error message if used without being in a try block.
(define throw (lambda (x) (display "No try clause is active!") (newline)))

(let* (
       ;Save the old containing try block
       (old-throw throw)
       ;we are saving the results in retval because we still have to clean up after
       ;ourselves before we can exit
       (retval (call-with-current-continuation
                 ;The exception will use this continuation to get back to the
                 ;remaining code
                 (lambda (k-exit-to-remaining-code)
                   (let (
                         ;This defines the exit handler
                         (error-handler
                           (lambda (exception)
                              (k-exit-to-remaining-code
                                 ;;error-handling code here
                                 ))))
                        ;This sets our error handler to be the official "throw" function
                        (set! throw error-handler)
                        ;;Regular code here
                        ;;You can throw an exception by doing:
                        (throw 'test-exception))))))

     ;Reinstate old try block
     (set! throw old-throw)

     ;Return the result
     retval)

This sets up the nesting so that throw always refers to the innermost try block. Now that you know what you want the code to look like, you can write a macro to take care of setting up all of the infrastructure.

Listing 11. Macro to generate exception code
;;This establishes the throw function
(define throw (lambda (x) (display "No try clause is active!") (newline)))

;;establishes syntax for a "try" block
(define-syntax try
  (lambda (x)
    (syntax-case x (catch)
      (
       (try expression (catch exception exception-handler-expression))
       (syntax
         (let* (
                (old-throw throw)
                (retval
                  (call-with-current-continuation
                    (lambda (k-exit)
                      (let (
                            (error-handler
                              (lambda (exception)
                                (k-exit exception-handler-expression))))
                           (set! throw error-handler)
                           expression)))))
               (set! throw old-throw)
               retval))))))

;;Short test suite

;Function that throws an error
(define (test-function)
  (throw 'ERROR))

;Function that does not throw an error
(define (test-function-2)
  (display "No error is generated.")
  (newline))

;Test out our try block
(try (test-function) (catch e (begin (display "Exception!  e is: ") (display e) (newline))))
(try (test-function-2) (catch e (begin (display "Exception!  e is: ") (display e) (newline))))

While we have covered most of our bases, there are still a few problems remaining. The problem comes in mixing continuations. For example, if, in addition to using continuations for exceptions, you also use them for other sorts of early exit logic, you would then have a problem. Take a look at the following code:

Listing 12. Bad interactions of continuations
;;Uses previously defined try/catch macro
(try
  (begin
    (call-with-current-continuation
      (lambda (kont)
        (try
          (kont 'value)  ;;jumps out of the continuation, but also skips resetting
                         ;;the active continuation
          (catch e (begin
                     (display "Interior exception handler.  Exception e is: ")
                     (display e)
                     (newline))))))
    ;;Because we didn't exit out through the try block in a normal fashion, this will
    ;;actually send us _back_ to the interior catch block the first time it is called!
    (throw 'ERROR))
  (catch e (begin
             (display "Exterior exception handler.  Exception e is: ")
             (display e)
             (newline))))

As you can see, the ability to jump around so freely can cause some difficulties in bookkeeping. To alleviate these problems, Scheme has a special procedure called dynamic-wind. dynamic-wind detects when a continuation jumps past a given stack frame. You can use this to reset the stack when a continuation jumps back and forth. dynamic-wind takes three arguments, each of which is a zero-argument procedure. The first one is the procedure to run anytime you enter a stack frame. The next one is the actual procedure to run. The final one is the procedure to run anytime you leave the stack frame. The following example gives a short trace of how dynamic-wind works:

Listing 13. Example using dynamic-wind
(let (
      (k-var (call-with-current-continuation
               (lambda (kont)
                 (dynamic-wind
                   (lambda () (display "Entering frame") (newline))
                   (lambda ()
                     (begin
                       (display "Running")
                       (newline)
                       (call-with-current-continuation
                         (lambda (inner-kont)
                           ;;Exit across the dynamic-wind boundary,
                           ;;saving the current continuation
                           (kont inner-kont)))))
                   (lambda () (display "Leaving frame") (newline)))))))
  (if (procedure? k-var)
      (k-var 'the-value)
      (begin
        (display k-var)
        (newline))))

First, it creates an outer continuation. Then, it enters the stack frame, calling the "entering" procedure. Then, it runs a procedure, producing a new continuation inside the dynamic-wind. This continuation is then returned across to the original continuation. However, because it crosses the dynamic-wind line, the "leaving" procedure is executed. Then, the inner continuation is executed again, which moves control across the dynamic-wind again, which calls the "entering" procedure again. It then returns back across the dynamic-wind, calling the "leaving" procedure again.

It's kind of a confusing call sequence, but it makes sense if you think of dynamic-wind as a "guard" line against far-reaching continuations. For flow control to pass across the "guard" line (whether by continuation or by normal control flow), the appropriate procedure must be executed for clean-up (either "entering" or "exiting," depending on the direction).

Using this, you can guard against some problems in the try macro. You can use dynamic-wind to reset which try/catch block the code is executing in. Here is the code:

Listing 14. Improved try/catch with dynamic-wind
;;This establishes the throw function as globally available, but displays an
;;error message if used without being in a try block.
(define throw (lambda (x) (display "No try clause is active!") (newline)))

;;establishes syntax for a "try" block
(define-syntax try
  (lambda (x)
    (syntax-case x (catch)
      (
       (try expression (catch exception exception-handler-expression))
       (syntax
          ;;Exit point using continuation k-exit
          (call-with-current-continuation
            (lambda (k-exit)
              (let (
                    ;;These are the two exception handlers, the old and the
                    ;;new.  Dynamic-wind sets them up and tears them down
                    ;;upon entering and exiting from scope
                    (old-throw throw)
                    (error-handler
                      (lambda (exception)
                        (k-exit exception-handler-expression))))
                   (dynamic-wind
                     ;;Entering scope -- set exception handler
                     (lambda () (set! throw error-handler))
                     ;;Actual processing
                     (lambda () expression)
                     ;;Exitting scope -- set exception handler to old value
                     (lambda () (set! throw old-throw)))))))))))

This version is actually shorter, and it satisfies both the original test cases and the one using continuations. Also, in case you think you are cheating by adding a new control structure with dynamic-wind, Kent Dybvig has shown that dynamic-wind can be implemented in terms of call-with-current-continuation.

We haven't covered all of the places where try/catch might produce unexpected behavior, but this is sufficient for most cases. The next section revisits some of the possible problems.

Generators using continuations

As previously mentioned, generators are a form of flow control. Python is the most commonly used language that implements generators. Let's think about how generators work and how continuations can be used to implement them.

First of all, a generator is created. This must necessarily entail the saving of a stack frame via either a continuation or a closure. Next, you return a value and bookmark your current position within the function. This also means that you need to have already bookmarked the place you are returning to.

So, our generator has two bookmarks, which are implemented with continuations. The first bookmark is a variable that is created when the generator is first created. This holds the position that the generator function is currently in. Then, when you run the generator, you also have a continuation that is the return point in the calling function. Right before returning, you create a continuation of the current location, and save it for the next invocation.

Now, let's look at the Scheme interface you might want for the Python-style generators:

Listing 15. Example of using a Python-style generator
(define-syntax generator
   (syntax-rules ()
     (
       (generator (yieldfunc) body ...)
       (let (
             (placeholder #f)  ;;placeholder in this function
             (return-proc #f)  ;;how to get out
             (finished #f))    ;;whether or not it is finished
         ;;this is the generator entrance
         (lambda ()
           (call-with-current-continuation
             (lambda (tmp-return-proc)
               ;;save the way out in "return-proc"
               (set! return-proc tmp-return-proc)
               (let (
                     ;;"yieldfunc" resets the placeholder and returns the value
                     (yieldfunc
                       (lambda (x)
                         (call-with-current-continuation
                           (lambda (current-placeholder)
                             ;;new placeholder
                             (set! placeholder current-placeholder)
                             ;;return value
                             (return-proc x))))))

                 ;;If the generator is done, return a special value
                 (if finished
                     'generator-finished

                     ;;If this is the first time, there will not be a
                     ;;placeholder established, so we just run the body.
                     ;;If there is a placeholder, we resume to that point
                     (if placeholder
                       (placeholder 'unspecified)
                       (let (
                             (final-value (begin body ...)))
                         (set! finished #t)
                         (return-proc final-value))))))))))))

(define sequence-generator
  ;;Initial parameters
  (lambda (start end increment)
    ;;"yield" will be used to generate a single value
    (generator (yield)
      ;;Main function body
      (let loop ((curval start))
         (if (eqv? curval end)
             curval
             (begin
                ;;yield the value
                (yield curval)
                ;;continue on
                (loop (+ curval increment))))))))

;;Example usage
(define my-sequence (sequence-generator 1 3 1))
(display (my-sequence))(newline)
(display (my-sequence))(newline)
(display (my-sequence))(newline)
(display (my-sequence))(newline)

The code is a little difficult to follow. It becomes even more complex if you add the ability to query whether or not the generator has any more values, or any other state query. Notice, though, the two generator-wide functions. One is the procedure to return to the calling program, and the other is the current location where the generator is executing. It may seem odd that the return procedure is stored in a generator-wide scoped variable, but it needs to be in order for the right version of the variable to be active after the continuation call. Otherwise, after the first call, it would revert to the version that was active when the placeholder continuation was created.

As mentioned earlier, problems might arise with exception-based continuations. Basically, the question is, if you have a try block when you start a generator and a try block when you run a generator and an exception is thrown in the generating function, which catch block is run? In the implementations I am using, the first catch block would be used. Is this the most intuitive way for this to happen? It depends on the individual case. However, these sorts of continuation-continuation interactions can be problematic, because it is not entirely clear what the "appropriate" action should be.

Backtracking using continuations

Finally, let's look at backtracking. The interface to your backtracker will be a function called amb. This function takes a list of values. For each value, amb sets up a backtrack bookmark. If the current value in the list doesn't work out (signified by calling the amb:fail function), the program "backtracks" to the last bookmark and tries a new value. A utility function called amb:assert signals amb:fail if the parameter is not true. Listing 16 shows these functions in use:

Listing 16. Using backtracking in Scheme
(let* (
      (a (amb 1 2 3 4 5 6 7))
      (b (amb 5 6 7 8 9 10)))
  (amb:assert (> a b))
  (display "a is ") (display a) (newline)
  (display "b is ") (display b) (newline))

The first time this function runs, it chooses 1 for a and 5 for b. Since a is not greater than b, it will fail and go back to the last bookmarked backtrack location. In this case, it will be in the assignment of b. It will then try each value of b. None of them will work, so it will then go and backtrack to the point where a is assigned. It will then try 2 with every value of b. It will continue like this until it finds a value of a that is greater than b, at which point it will continue on.

Listing 17 shows the implementation:

Listing 17. Implementation of backtracking
;AMB definition
(define amb:fail '*)

(define amb:initialize-fail
  (lambda x
    (set! amb:fail
      (if (null? x)
         (lambda () (error "amb tree exhausted!"))
         (car x)))))

(amb:initialize-fail)

(define amb
  (lambda alternatives
    (letrec (
             (amb-internal
               ;;"sk" returns the value (success continuation),
               ;;"alts is the list of values
	       (lambda (sk alts)
                 ;;fail if there are no alternatives
                 (if (null? alts)
                   (prev-amb-fail)
                   (begin
                      (call/cc
                        ;;"fk" is where to go when an
                        ;;alternative fails (failure continuation)
                        (lambda (fk)
                          ;;set the failure function to come back here
                          (set! amb:fail
                            (lambda ()
                              (set! amb:fail prev-amb-fail)
                              (fk 'fail)))
                          ;;return the first alternative
                          (sk (car alts))))
                      ;;We get here after a failure, so we
                      ;;run the function again using the rest
                      ;;of the list
                      (amb-internal sk (cdr alts))))))
             ;;this is the previous failure function
             (prev-amb-fail amb:fail))
      (call/cc
        ;;bookmark the way to the assignment into "sk"
        (lambda (sk)
          ;;go through each alternative
          (amb-internal sk alternatives)
          ;;After going through them, note that we have a failure
          (prev-amb-fail))))))

;;Calling "amb" without arguments will cause a failure.  This function
;;just makes it a little more obvious what is supposed to be happening.
(define amb:assert-failure
  (lambda () (amb)))

(define amb:assert
  (lambda (pred)
    (if (not pred) (amb:assert-failure))))

When reading the code, remember that the "failure continuation" is the way that the backtracker gets back into the list of values, and the "success continuation" is the way the function returns the next value back into the normal program flow.


Continuations: Everything you ever wanted from flow control

As you can see, you can use continuations to implement all sorts of advanced flow control statements. With just a few statements, you can build continuations into exceptions, generators, backtrackers, and other types of advanced flow control. But this article has only scratched the surface of what is available. With continuations you can also do things such as convert Web applications into a more traditional flow control structure and implement user-level threads. Unfortunately, many languages have not implemented continuations and thus leave their users without the many flow control features available. If a language only has continuations, it can implement the other advanced flow control features in a nearly trivial manner.

Resources

Learn

Get products and technologies

  • With IBM trial software, available for download directly from developerWorks, build your next development project on Linux.

Discuss

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Linux on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Linux
ArticleID=124297
ArticleTitle=Continuations and advanced flow control
publish-date=05242006