継続
継続(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 ==>