Naming clarification about delimited continuation (reset/3 and shift/1)

SWI-Prolog provides two predicates reset/3 and shift/1 to implement delimited continuations. These two predicates are described in Schrijvers et al., 2013 (preprint PDF), which is a very nice paper. However, the names seems a bit confused. I will explain below.

The reset and shift operators originally come from Danvy; Filinski,1990. “Abstracting Control”. In that paper, the authors claim that

Shift abstracts the current context as an ordinary, composable procedure and reset delimits the scope of such a context.

This means that the continuation captured by shift is like a “total function”, i.e. it always returns normally. But this is not the case in SWI-Prolog.

For example,

?- reset((
          format("111~n"),
          shift(1),
          format("222~n"),
          shift(2),
          format("333~n") 
         ),
         Term1, Cont1),
   format("444~n"),
   call(Cont1),
   format("555~n").
111
444
222
ERROR: reset `2' does not exist (No matching reset/3 call)

The ERROR tells us that Cont1 is not “total”, because there is no reset/3 as a delimiter inside Cont1. A workaround is manually wrapping a reset/3 around the continuation applications:

?- reset((
          format("111~n"),
          shift(1),
          format("222~n"),
          shift(2),
          format("333~n") 
         ),
         Term1, Cont1),
   format("444~n"),
   reset(call(Cont1), Term2, Cont2), % wrap a reset/3
   format("555~n").
111
444
222
555

This works. To obtain the original semantics of reset/shift, we can always do this, but still be careful with the semantics differences. In fact, the reset/3 and shift/1 in SWI-Prolog is more like the prompt and control in Felleisen,1988. The Theory and Practice of First-Class Prompts. The Racket reference demonstrates the differences.

(reset val) => val
(reset E[(shift k expr)]) => (reset ((lambda (k) expr)
                                     (lambda (v) (reset E[v]))))
  ; where E has no reset

vs.

(prompt val) => val
(prompt E[(control k expr)]) => (prompt ((lambda (k) expr)
                                         (lambda (v) E[v])))
  ; where E has no prompt

Note that, in the reset/shift case, there is a reset in the captured continuation (but SWI-Prolog no this stuff).

Below is a more complicated example to compare the differences:

  1. Prompt and control semantics:

    SWI-Prolog version:

    P.s. SWI-Prolog’s reset/3 and shift/1 is exactly prompt and control in Racket.

    :- dynamic save1/1.
    
    ?- reset((
              format("111~n"),
              shift(1),
              format("222~n"),
              shift(2),
              format("333~n") 
             ),
             Term1,Cont1),
       format("444~n"),
       assertz(save1(Cont1)).
    111
    444
    
    ?- save1(Cont1),
       reset((
              format("555~n"),
              call(Cont1),
              format("666~n")
             ),
             Term2, Cont2),
       format("777~n").
    555
    222
    777
    

    Note that the format("666~n") is not called, so Cont1 is not “total”.

    Racket version:

    > (define save1 null)
    
    > (match-let ([(cons v cont) (prompt (begin
                         (printf "111\n")
                         (control k (cons 1 k))
                         (printf "222\n")
                         (control k (cons 2 k))
                         (printf "333\n")
                         (cons '_ 0)
                         ))])
        (printf "444\n")
        (set! save1 cont))
    111
    444
    
    > (match-let ([(cons v cont) (prompt (begin
                         (printf "555\n")
                         (save1 '_)
                         (printf "666\n")
                         (cons '_ 0)
                         ))])
        (printf "777\n"))
    555
    222
    777
    
  2. Reset and shift semantics:

    SWI-Prolog version:

    P.s. Must wrap a reset/3 around the call(Cont1).

    ?- reset((
              format("111~n"),
              shift(1),
              format("222~n"),
              shift(2),
              format("333~n") 
             ),
             Term1,Cont1),
       format("444~n"),
       assertz(save1(Cont1)).
    111
    444
    
    ?- save1(Cont1),
       reset((
              format("555~n"),
              reset(call(Cont1), _, _),
              format("666~n")
             ),
             Term2, Cont2),
       format("777~n").
    555
    222
    666
    777
    

    Now the format("666~n") is called.

    Racket version:

    > (define save1 null)
    
    > (match-let ([(cons v cont) (reset (begin
                           (printf "111\n")
                           (shift k (cons 1 k))
                           (printf "222\n")
                           (shift k (cons 2 k))
                           (printf "333\n")
                           (cons '_ 0)
                           ))])
        (printf "444\n")
        (set! save1 cont))
    111
    444
    
    > (match-let ([(cons v cont) (reset (begin
                           (printf "555\n")
                           (save1 '_)
                           (printf "666\n")
                           (cons '_ 0)
                           ))])
        (printf "777\n"))
    555
    222
    666
    777
    

Therefore, the conclusion is that the reset/3 and shift/1 in SWI-Prolog is exactly prompt and control of Felleisen,1988.

Some useful quotes from Abstracting Control:

While the effects of these operators are very similar to operators control and prompt of [Felleisen 88], there is a significant semantical difference between shift/reset and control/prompt: the context abstracted by shift is determined statically by the program text, while control captures the context up to the nearest dynamically enclosing prompt. In general, this leads to different behavior.

The operator control is often referred to as Felleisen & Friedman’s Ƒ operator. Its operational definition leaves some room for variation, and one restricted form, also known as Ƒ-, is characterized by wrapping a prompt around all continuation applications. The effect is to close the extent of a context at the point of abstraction, instead of joining it to the context at the point of application. This is very much like building an explicit closure with FUNCTION around a LAMBDA in Lisp 1.5, which experience has shown to be preferable in general for both theoretical and practical reasons, leading to lexically scoped procedures in Scheme. In fact, Ƒ- coincides operationally with shift_1 for the simple case of one delimited context, just like static and dynamic scoping agree when the latter is used in a controlled way.

It seems that prompt and control can always simulate reset and shift, but not vice versa. SWI-Prolog’s delimited continuation is essentially prompt and control, that is nice. Perhaps prompt/3 and control/1 are more suitable names for delimited continuations in SWI-Prolog? Just for discussion.

2 Likes

Although I use reset/shift in my zdd library only as useful primitives for DCG like programming to hide computation states, I have little backgounds knowledge on the reset/shift, which is I feel shameful. Such said, I have some question and request on the current shift/reset of SWI-Prolog.

The swi reset may return 0 as the third argument, but
0 is not callable.

?- reset(true, A, X), reset(X, B, Y).
ERROR: Type error: callable' expected, found 0’ (an integer)
ERROR: In:
ERROR: [11] reset(user:0,_12328,_12330)
ERROR: [9] toplevel_call(user:user: …) at

So I needed a small interpreter to treat such 0 as true to continue. I could not find a reason why that 0 is replaced with true, which programming is easier including debugging.

My small experience might not be related to your post, but I am interested in reset/shift-like controls for programming to hide complex computation state like monad, though I am not familiar with monad so much.

Yes, reset/3 returns 0 as the third argument means that the Goal exit normally without calling shift/1.

I think it is reasonable, because it expresses there is no continuation instead of there is the continuation that includes a simple goal true. I always do some if-the-else test the Cont argument after calling reset/3.

Schrijvers et al., 2013:

Shift-less resets and reset-less shifts In hProlog, we have chosen to unify the Cont and Term arguments of reset/3 with zero in the absence of shift, and to have the toplevel catch shifts outside of a reset. Alternative semantics are easy to implement as a variation on the basic schema.

1 Like

My usage of reset/shift might be no more than a substitution of b_setval/b_getval, which may be only one of the aspects of original shift/reset. Thanks for the information.

Dear @chansey97, I am sorry for the confusion. You are right that the operators do not correspond to the traditional shift and reset operators in the literature.

I was not aware of the range of different operators at the time of choosing the names. Later, in my PPDP 2014 paper Heuristics Entwined with Handlers Combined I did indicate that the interface is closer to the fcontrol and run operators of Sitaram as presented in

D. Sitaram. Handling control. In Proceedings of the ACM SIGPLAN 1993 Conference on Programming Language Design and Implementation, PLDI ’93, pages 147–155, New York, NY, USA, 1993. ACM. ISBN 0-89791-598-4. . URL http://doi.acm.org/10.1145/155090.155104.

Which do you think is closer to SWI-Prolog’s interface: prompt/control or fcontrol/run?

Best,

Tom

3 Likes

Dear @TomSchrijvers, I didn’t know fcontrol/run before, thanks for telling me this operator. I just found that Racket has its implementation, which is called % and fcontrol.

The essential reduction rules are:

(% val proc) => val
(% E[(fcontrol val)] proc) => (proc val (lambda (x) E[x]))
; where E has no %

Yes. I think the %/fcontrol is closer to the current SWI-Prolog’s interface than prompt/control, because (fcontrol v #:tag prompt-tag) only requires one argument, ignore #:tag for now, which is more like shift/1 in SWI-Prolog, whereas (control id expr ...+) requires id.

In addition, % requires handler-expr, which can avoid Cont==0 and make the code shape more like catch/throw, but I have no idea if it would make the programs more readable…

P.s. I haven’t read that paper yet, so this is just a quick answer.

Dear @TomSchrijvers, recently I discovered another subtle difference between SWI-Prolog and other languages regarding the delimited continuations. That is we can yield a value from a “coroutine” (or more precisely the Goal argument of reset/3) to outside by shift/1, but we cannot safely transfer different values to the “coroutine” by multiple calling the same continuation.To some extent, our continuations become oneshot.

For example,

P.s. This example comes from the stackoverflow question. However his/her program cannot run directly in GHC, so I rewrote it to make it easier to test.

import Control.Monad
import Control.Monad.Trans
import Control.Monad.Trans.Cont

example :: IO Integer
example = evalContT . resetT $ do
           lift $ print "alfa"
           lift $ print "bravo"
           x <- shiftT $ \esc -> do
              lift $ print "charlie"
              lift $ esc 1
              lift $ print "delta"
              lift $ esc 2
              return 0
           lift $ print $ "zulu " ++ show x
           return $ x
           
λ> example
"alfa"
"bravo"
"charlie"
"zulu 1"
"delta"
"zulu 2"
0
The equivalent Racket program if someone wants to compare.
#lang racket
(require racket/control)

(define (example)
  (reset (begin
           (printf "alfa\n")
           (printf "bravo\n")
           (let ((x (shift esc
                  (printf "charlie\n")
                  (esc 1)
                  (printf "delta\n")
                  (esc 2)
                  0)))
             (printf "zulu ~a\n" x)
             x)
     )))

> (example)
alfa
bravo
charlie
zulu 1
delta
zulu 2
0

Note that it calls the continuation esc twice with 1 and 2. So how to write an equivalent program in SWI-Prolog? A naive idea is to have shift/1 yield a logic variable X, then outside unify it with a ground value before call(Cont). Something like:

example :-
  reset(step, ball(X), Cont),
  X=1, call(Cont),
  X=2, call(Cont).

step :-
  writeln("alfa"),
  writeln("bravo"),
  shift(ball(X)),
  format("zulu ~w~n", X).

but this doesn’t work: X already bound to 1, you cannot rebind it to 2!

One workaround is to use backtracking:

example :-
  reset(step, ball(X), Cont),
  ( writeln("charlie"), X=1, call(Cont), fail
  ; writeln("delta"), X=2, call(Cont)).

but it is obviously not ideal…

It would be nice if SWI-Prolog can unbind logic variables. Is it possible to unbind logic variables without backtracking? I found an old question in computer-programming-forum and Richard A. O’Kee’s answer is No, but is it possible in current SWI-Prolog? @jan


Anyway, below I provide a solution, which uses Attributed variables, but I’m not sure if it’s general enough.

newdc.pl

:- module(newdc,
          [ newdc_reset/4,
            newdc_shift/2,
            newdc_call/6,
            newdc_cont_val/2
          ]).

newdc_reset(Goal, Yield, Cont, ContSink) :-
  reset(Goal, ball(Yield, ContSink), Cont).

newdc_shift(Yield, ContSink) :-
  shift(ball(Yield, ContSink)).

newdc_call(Cont, ContSink, ContVal, NextYield, NextCont, NextContSink) :-
  put_attr(ContSink, newdc, ContVal),
  newdc_reset(Cont, NextYield, NextCont, NextContSink),
  del_attr(ContSink, newdc).

newdc_cont_val(ContSink, ContVal) :-
  get_attr(ContSink, newdc, ContVal).

test.pl

:- use_module(newdc).

example(X) :-
  newdc_reset(step, Yield, Cont, ContSink),
  writeln("charlie"),
  newdc_call(Cont, ContSink, 1, Yield2, Cont2, ContSink2),
  writeln("delta"),
  newdc_call(Cont, ContSink, 2, Yield3, Cont3, ContSink3),
  X=0.  

step :-
  writeln("alfa"),
  writeln("bravo"),
  newdc_shift(_, ContSink),    % --- Two calls newdc_shift 
  newdc_cont_val(ContSink, X), % --- and newdc_cont_val
  format("zulu ~w~n", X),
  newdc_shift(X, _).

?- example(X).
alfa
bravo
charlie
zulu 1
delta
zulu 2
X = 0.

The newdc_reset/4 and newdc_shift/2 are like reset/3 and shift/1 except that they receive an attribute variable ContSink that represents a channel from outside to the continuation. The newdc_call like the old call/1 except that it keep Danvy & Filinski’s original semantics of reset/shift, i.e. wrapping a reset/3 around the continuation applications.

Note 1: I had to introduce a new predicate newdc_cont_val/2. This is due to the Re-activation problem, see Schrijvers et al., 2013 (preprint PDF) Section 4.3, otherwise I could write the following:

%% ContVal is initialized before the call to shift/1, so it appears in the continuation, which is bad!
newdc_shift(Yield, ContVal) :- 
  shift(ball(Yield, ContSink)).
  get_attr(ContSink, newdc, ContVal).
  
step :-
  writeln("alfa"),
  writeln("bravo"),
  newdc_shift(_, X),  % --- One newdc_shift call
  format("zulu ~w~n", X),
  newdc_shift(X, _).

Note 2: To represent the return value semantics, the step/0 have to call newdc_shift/2 at the tail position.

I think I can answer the the stackoverflow question, but I don’t know if the solution is general enough.

Any advice/comment would be appreciated.

Hi @chansey97,

I am aware of the issue you describe. A simple work-around that I tend to use is to create copies of the continuation with fresh unification variables using copy_term/2. This is for instance used in the implementation of tabling.

Best,

Tom

1 Like

Your solution is remarkable.

example(X) :-
  reset(step, Ball, Cont),
  copy_term(Cont+Ball, Cont1+Ball1),
  copy_term(Cont+Ball, Cont2+Ball2),
  writeln("charlie"),
  ball(X1) = Ball1,
  X1=1, reset(Cont1, _, _),
  writeln("delta"),
  ball(X2) = Ball2,
  X2=2, reset(Cont2, _, _),
  X=0.  

step :-
  writeln("alfa"),
  writeln("bravo"),
  shift(ball(X)),
  format("zulu ~w~n", X),
  shift(ball(X)).

?- example(X).
alfa
bravo
charlie
zulu 1
delta
zulu 2
X = 0.

It works nicely. Thanks!

2 Likes