動機

整理continuation與CPS轉換

用法

cont可以當成一種很強的return或是goto,但是可以當成函數用

(let ((val (callcc (lambda (k) (k k)))))
  (set! cont k)
  (display k))

(cont 10)
(cont 20)

只要調用cont就會回去當初執行callcc的那一行,所以可以用它來跳來跳去

應用

dynamic-wind

類似prehook與posthook,只是有在裡面執行或是要到裡面,就一定會跑prehook與posthook

下面是其中一種實作,stack放(prehook . afterhook),之後就是對callcc動手腳,只要有人call就去跑整個stack

(define *here* (list #f))

(define origin-callcc call-with-current-continuation)

(define (call-with-current-continuation proc)
    (let ((here *here*))
        (origin-callcc (lambda (cont)
            (proc
                (lambda results
                    (reroot! here) ;; 包一層
                    (apply cont results)))))))

(define (dynamic-wind before during after)
    (let ((here *here*))
        (reroot! (cons (cons before after) here))
        (call-with-values during (lambda results
                                    (reroot! here)
                                    (apply values results)))))

(define (reroot! there)
    (if (not (eq? *here* there))
        (begin
            (reroot! (cdr there))
            (let ((before (caar there))
                  (after (cdar there)))
                (set-car! *here* (cons after before)) ;; reverse before after, so next time after will be invoke!!
                (set-cdr! *here* there)
                (set-car! there #f)
                (set-cdr! there '())
                (set! *here* there)
                (before)))))

exception

一個stack放fail時要跳回去那邊的cont

dynamic-wind是確保,cont被call時一定會pop stack 從cont進去時會把stack建回去 (在exception不需要就是了)

(define-syntax try 
  (syntax-rules (catch)
    ((_ exp ... catch proc) 
     ; =>
     (let ((cc (current-continuation)))
       (cond
         ((procedure? cc)
          (dynamic-wind 
           (lambda ()
             (set! exception-stack (cons cc exception-stack)))
           (lambda ()
             exp ...)
           (lambda ()
             (set! exception-stack (cdr exception-stack)))))
         
         ((pair? cc) 
          (proc (cadr cc))))))))

(define (throw exception-value)
  (let ((handler (car exception-stack)))
    (handler (list 'exception exception-value))))

(try (try (throw 'foo)
          catch
          (lambda (exn)
            (display "got inner exception: ")
            (display exn)
            (newline)
            (throw 'bar)))
     catch
     (lambda (exn)
       (display "got outer exception: ")
       (display exn)
       (newline)))

non-determing computing

一個stack放fail時要跳回去那邊的cont

(define (current-continuation) 
  (call-with-current-continuation 
   (lambda (cc)
     (cc cc))))

; fail-stack : list[continuation]
(define fail-stack '())

; fail : -> ...
(define (fail)
  (if (not (pair? fail-stack))
      (error "back-tracking stack exhausted!")
      (begin
        (let ((back-track-point (car fail-stack)))
          (set! fail-stack (cdr fail-stack))
          (back-track-point back-track-point)))))

; amb : list[a] -> a
(define (amb choices)
  (let ((cc (current-continuation)))
    (cond
      ((null? choices)      (fail))
      ((pair? choices)      (let ((choice (car choices)))
                              (set! choices (cdr choices))
                              (set! fail-stack (cons cc fail-stack))
                              choice)))))

(define (assert condition)
  (or condition (fail)))

; The following prints (4 3 5)
(let ((a (amb (list 1 2 3 4 5 6 7)))
      (b (amb (list 1 2 3 4 5 6 7)))
      (c (amb (list 1 2 3 4 5 6 7))))
  (assert (= (* c c) (+ (* a a) (* b b)))))

generator

一個ptr指向目前generator停下來的位置,另外為了讓generator好看,所以把生generator的cont交給make-yield

所以每次yield就會把val與cont丟回去

(define (current-continuation) 
  (call-with-current-continuation
   (lambda (cc)
     (cc cc))))

; void : -> void
(define (void)
  (if #f #t))

; tree-iterator : tree -> generator
(define (tree-iterator tree)
  (lambda (yield)
    (define (walk tree)
      (if (not (pair? tree))
          (yield tree)
          (begin
            (walk (car tree))
            (walk (cdr tree)))))
    
    (walk tree)))

; make-yield : continuation -> (value -> ...)
(define (make-yield for-cc)
  (lambda (value)
    (let ((cc (current-continuation)))
      (if (procedure? cc)
          (for-cc (cons cc value))
          (void)))))

; (for v in generator body) will execute body 
; with v bound to successive values supplied
; by generator.
(define-syntax for
  (syntax-rules (in)
    ((_ v in iterator body ...)
     ; => 
     (let ((iterator-cont #f))
       (letrec ((loop (lambda ()
                        (let ((cc (current-continuation)))
                          (if (procedure? cc)
                              (if iterator-cont
                                  (iterator-cont (void))
                                  (iterator (make-yield cc)))
                              (let ((it-cont (car cc))
                                    (it-val  (cdr cc)))
                                (set! iterator-cont it-cont)
                                (let ((v it-val))
                                  body ...)
                                (loop)))))))
         (loop))))))

(for v in (tree-iterator '(3 . ( ( 4 . 5 ) . 6 ) )) 
  (display v)
  (newline))

thread

前面是把cont塞到stack,這裡放到queue去!!

; thread-queue : list[continuation]
(define thread-queue '())

; halt : continuation
(define halt #f)

; void : -> void
(define (void) (if #f #t))

; current-continuation : -> continuation
(define (current-continuation)
  (call-with-current-continuation
   (lambda (cc)
     (cc cc))))

; spawn : (-> anything) -> void
(define (spawn thunk)
  (let ((cc (current-continuation)))
    (if (procedure? cc)
        (set! thread-queue (append thread-queue (list cc)))
        (begin (thunk)
               (quit)))))

; yield : value -> void
(define (yield)
  (let ((cc (current-continuation)))
    (if (and (procedure? cc) (pair? thread-queue))
        (let ((next-thread (car thread-queue)))
          (set! thread-queue (append (cdr thread-queue) (list cc)))
          (next-thread 'resume))
        (void))))

; quit : -> ...
(define (quit)
  (if (pair? thread-queue)
      (let ((next-thread (car thread-queue)))
        (set! thread-queue (cdr thread-queue))
        (next-thread 'resume))
      (halt)))
   
; start-threads : -> ...
(define (start-threads)
  (let ((cc (current-continuation)))
    (if cc
        (begin
          (set! halt (lambda () (cc #f)))
          (if (null? thread-queue)
              (void)
              (begin
                (let ((next-thread (car thread-queue)))
                  (set! thread-queue (cdr thread-queue))
                  (next-thread 'resume)))))
        (void))))

;; Example cooperatively threaded program
(define counter 10)

(define (make-thread-thunk name)
  (letrec ((loop (lambda ()
                   (if (< counter 0)
                       (quit))
                   (display "in thread ")
                   (display name)
                   (display "; counter = ")
                   (display counter)
                   (newline)
                   (set! counter (- counter 1))
                   (yield)
                   (loop))))
    loop))

(spawn (make-thread-thunk 'a))
(spawn (make-thread-thunk 'b))
(spawn (make-thread-thunk 'c))

(start-threads)

cont monad

monad就是消滅傳遞參數的過程,下面來複習cont monad

先基本的cps

(define (id x) x)
(define (fib1 n k)
    (if (<= n 1)
        (k 1)
        (fib1 (- n 1)
            (lambda (v1)
                (fib1 (- n 2)
                    (lambda (v2)
                        (k (+ v1 v2))))))))
(writeln (fib1 5 id))

把k往裡面推

(define (fib2 n)
    (if (<= n 1)
        (lambda (k) (k 1))
        (lambda (k)
            ((fib2 (- n 1))
                (lambda (v1)
                    ((fib2 (- n 2))
                        (lambda (v2)
                            (k (+ v1 v2)))))))))
(writeln ((fib2 5) id))

包一包,這裡可以注意到其實monad指的就是(lambda (k) ...),只要被這個包起來就是monad!!

(define (return val)
    (lambda (k) (k val)))
(define (bind m f)
    (lambda (k)
        ((m f) k)))
(define (fib3 n)
    (if (<= n 1)
        (return 1)
        (bind
            (fib3 (- n 1))
            (lambda (v1)
                (bind
                    (fib3 (- n 2))
                    (lambda (v2)
                        (return (+ v1 v2))))))))
(writeln ((fib3 5) id))

callcc就是

  1. 擷取現在的cont,塞進去lambda
  2. 讓原本的程式用原本的cont繼續跑
(define (callcc f)
  (lambda (k)
    ((f (lambda (val)
          (lambda (_)
            (k val))))
      k)))

邏輯上的關聯: callcc的type是?

cont吃一個值之後就跳走,所以她的return val是什麼type? 沒關西,先當成P -> Q

callcc是什麼type? 吃一個func,接回去原本的運算,注意到因為這裡的return val與cont吃得一樣,所以用一樣的type F -> P

F是什麼type? 吃一個cont,回傳某個值 (P -> Q) -> ?

某個值是什麼type? 這func要接回去原本的運算,所以是P

整個是((P -> Q) -> P) -> P

這就是Peirce’s law,如果把Q帶成bottom(Absurd),就可以得到排中律!!

CPS conversion

在cont monad看到monad怎麼把cont藏起來 接著就是怎麼把這個k自己生出來,下面都用untyped lambda calculus

說老實話,其實是為了這個才把這篇打出來的

另外如果有人說,學遞迴要用trace實際執行過程,直接丟這個給他看,看他怎麼trace 或是說遞迴可以轉成loop,也是丟這個,看他怎麼轉

naive

先是base case,變數與lambda,這裡base case叫atomic 變數直接回傳,lambda要幫他開一個洞,把值傳回去

(define (M expr)
  (match expr
    [`(λ (,var) ,expr)
      (define $k (gensym '$k))
     `(λ (,var ,$k) ,(T expr $k))]
    [(? symbol?)
      expr]))

剩下就是apply,每個都要先轉沒問題,那這裡要放什麼? 可以先看看簡單的case: (f v)且都是變數 最保險就是每個都要轉(過M),

((λ (a)
  ((λ (b)
    (k (a b))) ;; !!
      v))
  f)

等等,現在a會多一個cont

((λ (a)
  ((λ (b)
    (a b k)) ;; good
      v))
  f)
(define (T expr k)
  (match expr
    [`(λ . ,_) `(,k ,(M expr))]
    [(? symbol?) `(,k ,(M expr))]
    [`(,F ,V)
      (define $f (gensym '$f))
      (define $v (gensym '$v))
      (T F `(λ (,$f)
              ,(T V `(λ (,$v)
                       (,$f ,$v ,k)))))]))
(T '(g a) 'halt) 

變成

((λ ($f1445) 
  ((λ ($e1446) 
    ($f1445 $e1446 halt)) a)) g) 

high-order

最保險的方式可以work,但output有點長,為什麼不直接apply進去?

怎麼直接apply進去? 我們不是有現成的函數嗎

重新看T的base case,這裡如果可以直接apply函數,就可以把那坨去掉 換言之,我們原本在k傳的是symbol,但現在是racket的function,最後回傳symbol 所以先把quote拿掉

(define (T expr k)
  (match expr
    [`(λ . ,_) (k (M expr))]
    [(? symbol?) (k (M expr))]
    [`(,F ,V)
      (T F (λ (f)
              (T V (λ (v)
                       `(,f ,v ,k)))))])) ;; !!

等等,這裡k是racket的lambdaㄟ 這裡要是quote,所以要把(f v)的拉到原本的k去

(define (T expr k)
  (match expr
    [`(λ . ,_) (k (M expr))]
    [(? symbol?) (k (M expr))]
    [`(,F ,V)
      (define $ret (gensym `$ret))
      (define cont `(λ (,$ret) ,(k $ret)))
      (T F (λ (f)
              (T V (λ (v)
                       `(,f ,v ,cont)))))]))

回來看M,會看到lambda的k要改,改成racket的lambda,最後回傳symbol

(define (M expr)
  (match expr
    [`(λ (,var) ,expr)
      (define $k (gensym '$k))
     `(λ (,var ,$k) ,(T expr (λ (ret) `(,$k ,ret))))]
    [(? symbol?)
      expr]))
(T '(g a) (λ (ans) `(halt ,ans)))

變成

(g a (λ ($rv1) (halt $rv1))) 

少很多,但能不能更好,那個lambda很多餘

混合

多的lambda來自T的cont,但又不能直接去掉,有什麼辦法? 回去看怎麼用的,不覺得很怪嗎,明明是轉symbol卻要寫lambda,有沒有辦法變成最初的方式去call

所以第一層可以先抄第一種T

(define (T-symbol expr k-symbol)
  (match expr
    [`(λ . ,_) `(,k-symbol (M expr))]
    [(? symbol?) `(,k-symbol (M expr))]
    [`(,F ,V)
      ... ]))

但apply用第二種T

(define (T-symbol expr k-symbol)
  (match expr
    [`(λ . ,_) `(,k-symbol ,(M expr))]
    [(? symbol?) `(,k-symbol ,(M expr))]
    [`(,F ,V)
      (T-func F (λ (f)
              (T-func V (λ (v)
                       `(,f ,v ,k-symbol)))))]))

再抄第二種T

(define (T-func expr k-func)
  (match expr
    [`(λ . ,_) (k-func (M expr))]
    [(? symbol?) (k-func (M expr))]
    [`(,F ,V)
      (define $ret (gensym `$ret))
      (define cont `(λ (,$ret) ,(k-func $ret)))
      (T-func F (λ (f)
              (T-func V (λ (v)
                       `(,f ,v ,cont)))))]))

M要抄第一種,觀察兩個T的base case,會看到(M expr),最後都是要被展開成symbol 所以選都是symbol的第一種

(define (M expr)
  (match expr
    [`(λ (,var) ,expr)
      (define $k (gensym '$k))
     `(λ (,var ,$k) ,(T-symbol expr $k))]
    [(? symbol?)
      expr]))
(T-symbol '(g a) 'halt)

變成

(g a halt)

partition

這個是為了還原calling stack而產生的

要還原calling stack需要知道誰是caller,與之後衍生的 所以回去看生出cont的部分(幫lambda多一格的部分)

(define (T-func expr k-func)
  (match expr
    [`(λ . ,_) (k-func (M expr))]
    [(? symbol?) (k-func (M expr))]
    [`(,F ,V)
      (define $ret (gensym `$ret)) ;; 1
      (define cont `(λ (,$ret) ,(k-func $ret)))
      (T-func F (λ (f)
              (T-func V (λ (v)
                       `(,f ,v ,cont)))))]))

(define (M expr)
  (match expr
    [`(λ (,var) ,expr)
      (define $k (gensym '$k)) ;; 2
     `(λ (,var ,$k) ,(T-symbol expr $k))]
    [(? symbol?)
      expr]))

1是起點,也就是apply;2是中間。 所以把1的symbol標成user, 2是cont

(define (T-func expr k-func)
  (match expr
    [`(λ . ,_) (k-func (M expr))]
    [(? symbol?) (k-func (M expr))]
    [`(,F ,V)
      (define $ret (genusym `$ret)) ;; 1
      (define cont `(λ (,$ret) ,(k-func $ret)))
      (T-func F (λ (f)
              (T-func V (λ (v)
                       `(,f ,v ,cont)))))]))

(define (M expr)
  (match expr
    [`(λ (,var) ,expr)
      (define $k (genksym '$k)) ;; 2
     `(λ (,var ,$k) ,(T-symbol expr $k))]
    [(? symbol?)
      expr]))

在ref中還有scheme的轉換,但其實就是多了不定數量參數列的處理

TODO

delimit cont 據說cooperative的thread加macro可以變成preemptive! Closure conversion: How to compile lambda

Ref

Continuations by example: Exceptions, time-traveling search, generators, threads, and coroutines Appendix: An implementation of dynamic-wind 如何解释 Lisp 中 call/cc 的概念? 用call/cc合成所有的控制流结构 How to compile with continuations