CPS 継続渡しスタイル がわかってきた。

range 関数を考える。

シンプルに、

(define (range a b)
  (if (> a b) 
      []
      (cons a (range (+ a 1) b))))

これは、最後に呼び出される関数が cons なので、末尾再帰ではない。

末尾再帰にすると、

(define (range a b l)
  (if (> a b)
      (reverse! l)
      (range (+ a 1) b (cons a l))))

これだと reverse! の分だけ遅くなるので、本当は逆から作るといい。

さあ、CPSだ!

(define (range a b cont)
  (if (> a b)
      (cont [])
      (range (+ a 1) b (lambda (x) (cont (cons a x))))))

本題はここから。
(lambda (x) (cont のあたりで、なんともいえない違和感を感じないか?

さて、(range 1 4 (lambda (x) (print x)))をほぐすと、

((lambda (x)
  ((lambda (x)
     ((lambda (x)
	((lambda (x)
	   ((lambda (x)
	      (print x))
	    (cons 1 x)))
	 (cons 2 x)))
      (cons 3 x)))
   (cons 4 x)))
 [])

となる。やはり違和感がある。
実はこれは、

((lambda (x) (print x))
 ((lambda (x) (cons 1 x))
  ((lambda (x) (cons 2 x))
   ((lambda (x) (cons 3 x))
    ((lambda (x) (cons 4 x))
     [])))))

を還元した形だ。ここまでなおすと、違和感が消える。それに、スタックに

cons 4
cons 3
cons 2
cons 1
print

と積んだ様子を、連想しやすい。

CPSだと強力な状態遷移機械(オートマトン)が作れる、コンパイラにとって重要だというのは、これが、スタックを使うプッシュダウン・オートマトンと同じだからだ。

CPSの修得が難しいのは、一段階還元された形を扱うために、違和感があるからだ。

CPSは、スタックをクロージャに保存しているだけともいえる。
最初のコレを見て ・・・(1)

(define (range a b)
  (if (> a b) 
      []
      (cons a (range (+ a 1) b))))

CPSのコレが ・・・(2)

(define (range a b cont)
  (if (> a b)
      (cont [])
      (range (+ a 1) b (lambda (x) (cont (cons a x))))))

イメージできるようになろう。
(1)は、範囲の長さ分、スタックを消費する。
(2)は末尾再帰だからスタックは消費しないが、範囲の長さ分、クロージャが作られる。
(1)と(2)の計算量のオーダーは同じだ。