[Prev]    [ Up ]    [Next]

継続

継続(continuation)とは,式を評価している途中のある時点で,『いま得られた 値を使って,この後は何を計算するのか』を表すものである.たとえば,Scheme の関数呼びだしの式を評価する際には,まず関数とその引数を評価して,その 後で関数に引数を適用する.

	==> (+ (* 1 2) (* 3 4))
	;; ==> (+ 2 12)
	;; ==> 14
	14
この式の場合,まず「+」,「(* 1 2)」,「(* 3 4)」を評 価したのち,「(+ 2 12)」を評価する. 各部分式の評価が左から右へ進むものとすると, たとえば,「(* 3 4)」を評価した後にするべき計算,つまり継続は,
        『いま得られた値に2を加える』
である.この式の評価を完了するためには, Schemeのシステムは,この継続を知っていなければならない. 継続は,「何を評価して,その値によって次には何を行う」というように,どう いう手順で計算を進めるか,つまりプログラムの実行順序を制御する概念であり, Schemeに限らず,どのプログラミング言語にも欠かせないものである. しかし,通常,プログラマは継続を意識することはない.

Schemeでは,継続は単なる概念ではない. Schemeでは,継続をデータとして生成し,明示的に取り扱うことができる. 継続を利用することによって,さまざまな実行順序制御が可能になる. これは,Schemeの大きな特徴の一つである. 他の大多数のプログラミング言語では,プログラマが継続を取り扱うことはでき ない.

Schemeでは,継続は1引数関数として実現される.たとえば,上の例の場合の 継続

       『いま得られた値に2を加える』
は,ある値に2を加える関数
	(lambda (x) (+ 2 x))
と表現できる.この関数に(* 3 4)を引数として与えれば,
	==> ((lambda (x) (+ 2 x)) (* 3 4))
	14
のように,
	(+ (* 1 2) (* 3 4))
を評価したのと同じ値が得られる.

継続の生成には,関数call-with-current-continuationを用いる. この長い名前の省略形としてcall/ccが用意されている処理系も多い. なお,scmでは,call/ccは定義されていないので,

        (define call/cc call-with-current-continuation)
としておくとよい. call-with-current-continuation(以下,call/cc)は,1引数関数funcを引数とする.
	(call/cc func)
call/ccが呼び出されると,その時点での継続が生成され,その継続を引数とし て関数funcが実行される. 以下,関数呼び出しの評価は,まず関数を評価,続いて引数を左から順に評価 して,最後に関数に引数を適用する,というように行うものとする.
	==> (+ (* 1 2) (call/cc (lambda (c) (* 3 4))))
	14
この例では,call/ccによって,『値に2を加える』という継続が生成され, この継続が,関数
	(lambda (c) (* 3 4))
の引数cに渡される.ただし,この関数の中では,継続cを呼び出していない.この場合は,この関数を評価した結果が,そのままcall/ccの値となる.したがって,
	==> (+ (* 1 2) (call/cc (lambda (c) (* 3 4))))
	14
という結果となる. 一方
	==> (+ (* 1 2) (call/cc (lambda (c) (* 3 (c 4)))))
	6
のように,call/ccで呼び出した関数において,継続を呼び出すと, 関数の実行を直ちに終了して,継続への引数をcall/ccの値として,継続を実行 する.

継続は関数として実現されるので,いったん生成した継続に名前をつければ, それをいつでも呼び出すことができる.

	==> (define cont #f) ;; 大域変数contを定義する(#fは適当な値)
	cont
	==> (+ (* 1 2) (call/cc (lambda (c) (set! cont c) (* 3 4))))
	14
この例では,call/ccによって,『値に2を加える』という継続が生成される. この継続が関数
	(lambda (c) (set! cont c) (* 3 4))
の引数cに渡される. この関数では大域変数contにcを代入している. cを呼び出してはいないので,この関数の値がcall/ccの値になる. 生成された継続が大域変数contにバインドされているので,contを呼び出せば生 成した継続を実行することができる.継続は『値に2を加える』であったので
	==> (cont 2)
	4
	==> (cont 5)
	7
などのようになる. また別の例を挙げる.
        ==> (define x 0)
        ==> (define y 2)
        ==> (define cont #f)
        ==> (list x (call/cc (lambda (c) (set! cont c) 1)) y)
	(0 1 2)
        ==> (cont 3)
	(0 3 2)
        ==> (set! x 4)
        ==> (cont 3)
	(0 3 2)
        ==> (set! y 6)
        ==> (cont 3)
	(0 3 6)
この例でcall/ccによって生成される継続は, 『0,いま得られた値,yを評価した値からなるリストを生成する』 となる.

継続を利用した実用的な例の一つとして,大域脱出が挙げられる. たとえば,関数が再帰的に呼び出されているとき,通常,呼び出された関数は,呼び出した側へ 値を順番に返していく.

       ;; リストの要素の積を求める関数(call/ccは利用していない)
        ==> (define (mul x) 
	;; displayとnewlineは途中経過を表示するためのもので
	;; 計算に必要なものではない.
	;; また再帰呼び出し終了後の状態をあわせて表示するために
	;; let式を用いている.
	  (display x)
	  (newline)
	  (let ((result
	    (if (null? x)
	        1
		(* (car x) (mul (cdr x))))))
	    (display (list x result))
	    (newline)
	    result))
        ==> (mul '(1 2 3 4))
	(1 2 3 4)
	(2 3 4)
	(3 4)
	(4)
	()
	(() 1)
	((4) 4)
	((3 4) 12)
	((2 3 4) 24)
	((1 2 3 4) 24)
	24
	;; 0を含むリストの場合,0が現れたら,その後は計算する必要はないが,
        ;; この関数では逐一計算を行う.
        ==> (mul '(1 0 3 4))
	(1 0 3 4)
	(0 3 4)
	(3 4)
	(4)
	()
	(() 1)
	((4) 4)
	((3 4) 12)
	((0 3 4) 0)
	((1 0 3 4) 0)
	0
このような関数の再帰的な呼び出しの関係を順番にたどることな く,関数の呼び出し関係を一気に遡って,途中の処理をスキップして処理を 再開することを大域脱出という.大域脱出は例外処理に用いられる.
       ;; リストの要素の積を求める関数(call/ccを利用する)
        ==> (define (mul-cont x)
	  (call/cc (lambda (escape) 
	     (letrec ((mul-aux (lambda (y)
	                        (display y)
				(newline)
				(let ((result
    				       (cond ((null? y) 1)
				             ((= (car y) 0) (escape 0))
					     (#t (* (car y) (mul-aux (cdr y)))))))
			           (display (list y result))
				   (newline)
				   result)
				 )))
	       (mul-aux x)))))
        ==> (mul-cont '(1 2 3 4))
       (1 2 3 4)
       (2 3 4)
       (3 4)
       (4)
       ()
       (() 1)
       ((4) 4)
       ((3 4) 12)
       ((2 3 4) 24)
       ((1 2 3 4) 24)
       24
       ==> (mul-cont '(1 0 3 4))
       (1 0 3 4)
       (0 3 4) ;; carが0のときに継続を呼び出して大域脱出する.
       0
同様の例を挙げる.
        ;; sumup
	;; 引数のリストの要素が全て数値であれば,それらの和を返す.
	;; そうでないものがあれば,そのような要素のうち最初のものを返す.
	==> (define (sumup x)
	    (call/cc (lambda (cont) 
		       (letrec ((sum-aux (lambda (y)
					   (cond ((null? y) 0)
					         ((not (number? (car y))) (cont (car y)))
					         (#t (+ (car y) (sum-aux (cdr y))))))))
		         (sum-aux x)))))

	==> (sumup '(1 33 4 0 3 7))
	48
	==> (sumup '(1 0 -1 a 7))
	a

このようにcall/ccで呼び出した関数内で継続を使うことによって,その関数の実行から脱出することができる.

さらに次も大域脱出のために継続を利用した例である.
	==> (define (read-eval-print)
	    (display (call/cc 
		      (lambda (q)
		        (letrec ((loop 
				  (lambda (x) 
				    (if (eq? x 'end)
				        (q 'bye)
				        (display (eval x)))
				    (newline)
				    (display "# ")
				    (loop (read)))))
			  (display "# ")
			  (loop (read))))))
	(newline))
	==> (read-eval-print)
	# (car '(a b c))
	a
	# (+ 3 6)
	9
	# end
	bye
	==> 

[Prev]    [ Up ]    [Next]