What does "with continuation" mean?

The help for the run and call with continuation blocks just say that it's too complicated. I want to know what a continuation is. Reading the manual left me still a little confused as to what continuations are.

1 Like

Hi, welcome to the forum! I've taken the liberty of moving your question to the Computer Science topic because you're really asking about the history of programming language design, more than about :sn: specifically. I'm going to tell you probably more than you want to know; feel free to pick the juicy parts to read.

PART THE FIRST.

So, first of all, know that you're taking on the most challenging feature of :sn:. Plenty of professional adult programmers (in any language) get through their careers with no clue what a continuation is. :~)

PART THE SECOND.

The continuation of any procedure call is "what work remains to be done when this procedure call finishes?" So, if you do
co1
and the + procedure is about to return the value 9, what work remains to be done? Answer: Multiply 3 by the result, and have the sprite say the result of the × procedure.

That's a particularly simple continuation. If the call to + were inside the definition of another procedure, which was called from another procedure, and so on, the continuation would include everything that remains to be done until the toplevel script has finished.

Every programming language has continuations, but in most languages they're not first class -- they're not exposed to the language user as things that can be manipulated as data. Instead, the implementors of those languages talk about the stack, which is a record of all the procedure calls in progress. Each procedure call generates a stack frame. So, to be precise, a continuation is a stack, not including the currently active stack frame.

PART THE THIRD.

Scheme (God's programming language, of which :sn: is a dialect) introduced continuations as first class data. Since procedures are first class in Scheme, a continuation is embodied in the form of a procedure which, when called, will do the tasks remaining to be done. So the continuation of the + block in the above is this:
co2
or, if you prefer.


(making the input to the procedure explicit).

You're probably now thinking, "this is too easy for all the fuss about how hard it is." That's because in this simple example the + procedure is called directly from a toplevel script, so you can see its entire continuation surrounding it in the program. Also, in this simple example there aren't any variables involved.

So, in the script
co4
what's the continuation of the second call to the MOVE block? (Remember, it's procedure calls, not procedures, that have continuations.)

This time the continuation isn't visible on the screen. It's this:
co5
After the second MOVE finishes, we still have the second TURN to do, plus the loop for values of I from 3 to 10.

But in reality, the language doesn't construct that picture. It doesn't have to construct anything, because the stack already knows what step comes next in the script, and what the value of I is (namely 2) during the second call to MOVE. What's really inside the continuation is a tiny piece of code that puts the remembered stack in the computer's stack register and then returns to the next thing down on the stack.

PART THE FOURTH.

CALL WITH CURRENT CONTINUATION (the :sn: name leaves out "current" just to make it shorter) is what makes continuations. That is, when called, it takes its own continuation -- what's on the stack during this call -- and makes a data structure out of it, in the form of a procedure. It then calls the procedure given to it as input, with the continuation as that procedure's input.

One of the things that makes call/cc (that's the official abbreviation in Scheme) complicated is that there are so many procedures involved:

  • The procedure calling call/cc.
  • The call/cc itself.
  • The procedure given to call/cc as input.
  • The continuation, which is embodied as a procedure.

It's easy to get these mixed up. But that's just a bookkeeping confusion; it's the idea of turning a stack frame into a procedure that's the intellectually demanding part.

PART THE FIFTH.

Now for some history. Back in the dinosaur days, people programmed computers in machine language -- programs made of the instructions built into the computer hardware. The first higher-level language that became widely used was Fortran ("formula translator"). It's still used today, in fact, although modern Fortran is quite differerent from the early version. ("Higher level" means that one Fortran statement would be translated into more than one machine language instruction.)

In Fortran, one of the statement types is GO TO. This takes a pointer to another statement as input, and does that statement next, instead of the one after the GO TO (and then continues with the one after the target statement, etc.). All the looping constructs, things like FOR and WHILE and so on, would be built out of GO TOs.

The trouble with GO TO is that it would let you jump to anywhere in the program, even to someplace that expects different kinds of frames on the stack from what's on the stack at the point of the GO TO. To take a simple example, if the toplevel program does a GO TO to the inside of a procedure, and then that procedure tries to return, it will try to remove a frame from the stack, but the stack doesn't have any frames on it, and the program will blow up.

Since Fortran was the first higher level language, there wasn't any computer science theory about programming languages when it was designed. But then people started inventing other languages, and then computer science was invented, and people started building theories about programming languages. And pretty early on, the computer scientists started complaining about GO TO, and about those bugs people would have by jumping into the middle of a procedure. An influential journal article of that time was called "GO TO Considered Harmful." (To this day, CS nerds think it's funny to write emails with subject lines like "Computer Science Considered Harmful.") And people started inventing languages without GO TO.

And yes, you can prove that if your language has a conditional construct (IF or something similar) and a looping construct (REPEAT or something similar), then you can express any algorithm without needing GO TO. But there were still situations in which it was much easier to express the algorithm if you did have GO TO. So, the question arose, is there a way we can have the expressive power of GO TO without the horrible bugs? And the answer is, yes, if the input to GO TO is not just the address of another instruction but also a stack captured from that address, so that returning from the procedure will return someplace sensible. And that's what a continuation is. Call/cc is much the the label that can be used as the target of a GO TO, and the continuation made by the call/cc is the GO TO itself.

PART THE SIXTH.

What are the situations in which people really wished they had GO TO when programming in those Considered Harmful languages? There are two main ones:

  1. Nonlocal exit. CATCH and THROW, or SETJMP and LONGJMP in C. This is the situation in which you're four deep in nested loops and you want to jump out from all of them. So, CATCH is implemented as call/cc, and THROW takes a continuation as input and calls it.

  2. Multithreading. You have six things going on at once, and every tenth of a second or so, you want to switch from one to another. Each thread is represented by a continuation, and the scheduler keeps a list of active threads.

There are others, but those two cover a lot of ground. In the :sn: manual you'll find implementations of both of these cases.

If any of that's not clear, ask a more specific question.

1 Like

Thank you! Since I'm using Snap!, I had to ignore these blocks, because I didn't understand them. Now I can maybe solve some problems that caused me quite a headache! :D

There is a question I got while experimenting with the catch/throw blocks of the "Iteration/composition" library: What is it with the inputs? I got that the first input reports the blocks coming after the run w/ continuation block, but all inputs after that one simply report 0. How can I get the other inputs to report anything but 0, if it's possible?

I don't understand this. What do you mean by "all the inputs"? CATCH has the upvar whose value is a continuation plus a script or expression to evaluate. THROW has one input whose value should be a catch tag (the upvar, dragged in from the CATCH block) and, if a reporter, the value you'd like to use in place of the expression.

I see how it got confusing. The image should explain what I mean!

Snap with continuation|539x239

You can't get the other inputs to report anything other than 0. It's like this:
untitled script pic (2)
but "Inputs are quite nice" is replaced by the continuation.

This is kind of confusing. What would you use
[scratchblocks]
(call w/continuation() :: control)
[/scratchblocks]
for?

The most common uses are for nonlocal exit ("break") and multithreading. There are examples of both in the Reference Manual.

I'm happy to explain more, but it'd help a lot if you first read Chapter X (that's ten, not a variable) of the Manual, and if you get stuck, ask a specific question.