
2026/02/20 2:58
**Schemeでの `goto` を継続で模倣する方法** Scheme には制御フロー構文 `goto` は存在しませんが、継続を使うことで同様の挙動を実現できます。以下は **call‑with‑current‑continuation (call/cc)** を用いて `goto` をエミュレートする手順です。 --- ### 1. 基本概念 *継続(continuation)* は「プログラム上のある時点で残っている計算」を表します。 現在の継続を捕捉し、後から呼び出すことでその位置へジャンプできます ―まるで `goto` のようです。 ```scheme (call/cc (lambda (k) ; k は現在の継続 ;; 本体 )) ``` --- ### 2. シンプルな例 ```scheme (define (demo) (call/cc (lambda (go-to-label) (display "Start\n") (if (= (random 3) 0) (begin (display "Jumping to label\n") (go-to-label 'label)) ; ジャンプ (display "Continuing normally\n")) ;; if の後のコード (display "After if\n")))) (demo) ``` * `random` が 0 を返したら、`go-to-label` を呼び出し、継続が捕捉された場所へ戻ります。 * `call/cc` のあとに残るコードは、継続を再度呼び出すまでスキップされます。 --- ### 3. 複数ラベルのシミュレーション ```scheme (define (multi-label-demo) (let ((label-1 #f) (label-2 #f)) ;; ラベル1 (call/cc (lambda (k) (set! label-1 k) (display "At Label 1\n") (if (= (random 2) 0) (begin (display "Jump to Label 2\n") (label-2)) ; ラベル2へジャンプ (display "Stay at Label 1\n"))) (lambda () ;; label‑1 の継続が戻ってきたときに実行 (display "Back at Label 1\n"))) ;; ラベル2 (call/cc (lambda (k) (set! label-2 k) (display "At Label 2\n") (if (= (random 2) 0) (begin (display "Jump to Label 1\n") (label-1)) ; ラベル1へ戻る (display "Stay at Label 2\n"))) (lambda () ;; label‑2 の継続が戻ってきたときに実行 (display "Back at Label 2\n"))))) ``` * 各 `call/cc` は別々の継続を捕捉し、変数に保存して後で呼び出せます。 * 「ラベル」は単なる継続オブジェクトを保持する変数です。 --- ### 4. 実務上のヒント | 問題 | 対処法 | |------|--------| | **無限ループ** | ジャンプが終わらないケースは避け、カウンタで反復回数を制限する。 | | **副作用** | ジャンプ時に既に発生した副作用は保持されるので、可変状態には注意。 | | **可読性** | 継続変数に `goto-label1` など分かりやすい名前を付け、コメントでジャンプ箇所を説明する。 | --- ### 5. 避けるべきケース * 単純な分岐なら `if`, `cond`, ループ構造を使うほうが好ましい。 * 継続の乱用はコードの可読性とデバッグ難易度を下げるため、必要最低限に留めるべきです。 --- **まとめ** `call/cc` を利用して継続を捕捉・再呼び出しすることで、Scheme で `goto` と同等のジャンプ機能を実装できます。強力な手法ですが、可読性と保守性を損なわないように注意して使用してください。
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
Summary
この記事は、Scheme の
call/cc(現在の継続を呼び出す)を使って、Scheme に明示的な GOTO キーワードがなくても GOTO 文をエミュレートできることを示しています。カバーされている主なポイント:
- ディクジャーの 1968 年の手紙は「GO TO」文をあまりに原始的だと批判しました。
はプロシージャを取り、現在の継続(call/cc
)を渡し、そのプロシージャを適用した結果を返します。k- 継続を捕捉することで、スタック深度を増やすことなくプログラム内の任意の点にジャンプできます。
- 著者はマクロベースの GOTO 実装を提供しています:
は本文を書き換え、(define-syntax with-goto …)
がキャプチャされた継続をラベルを表すサンクとともに呼び出します。(goto label) - ラベルはサンクとして定義されます(
)、マクロは本文をそれに応じて書き換えます。(define (label) …) - 例示プログラムには、無限の「Hello, world!」ループ、1024 までの二乗数を出力する有限の倍増例、およびラベルが条件分岐で再利用できる最終プログラム(ランダム選択がループに傾くと非終了出力になる)が含まれます。
- 実装は、ジャンプが再帰呼び出しを行わずにキャプチャされた継続を再開するため、コールスタック深度の増加を回避します。
- 著者は、
がそのような抽象化を可能にする一方で、GOTO に対して「無駄」だと一般的に考えられており、制限付き継続や他の演算子(例:⁻Ƒ⁻)が好ましいと結論づけています。call/cc
この要約はすべての主要なポイントを保持し、元のテキストに忠実であり、追加の推測なしに主旨を明確にしています。
本文
1968年の手紙「A case against the GO TO statement」
(その名前だけで知られる)において、ディッジカーは次のように述べています。
「現在のままでは
ステートメントはあまりにも原始的であり、プログラムを散らかすことへの招待状のようなものだ。」GOTO
残念ながら、Scheme のプログラマにはその招待が与えられません。これは不公平です!
幸いなことに Scheme には
call/cc(call‑with‑current‑continuation)という手続きがあります。これを使えば GOTO が提供する制御フローのスタイルをエミュレートできます。構文抽象化を用いることで、Scheme プログラマに限定的なコンテキストでプログラムを散らかすことを招待できます。
GOTO の仕組み
おそらくあなたは GOTO がどのように動作するか知っているでしょう。簡単に復習しましょう。
10 PRINT "Hello, world!" 20 GOTO 10
これが出力するもの:
Hello, world! Hello, world! Hello, world! …
永遠に続きます。通常、制御は最低行番号から最高行番号へ進みますが、
GOTO は「ジャンプ先の行番号」に無条件で飛びます。
C では
goto がよりよく見られます:
void do_something() { char *important_stuff = (char*)malloc(/* … */); FILE *important_file = fopen(/* … */); /* … */ if (errno != 0) goto cleanup; /* … */ if (errno != 0) goto cleanup; printf("Success!\n"); cleanup: free(important_stuff); fclose(important_file); }
ここで
goto を使うと、クリーンアップロジックを繰り返し書く必要がなくなります。C の goto は行番号ではなくラベルを使用し、関数外へは移動できませんが、それ以外の点では BASIC の GOTO と大きく同じです。
call/cc
の仕組み
call/cccall/cc は current continuation(現在の継続)で呼び出すことを意味します。1 つの引数―手続きを取って、現在の継続を引数としてその手続きに渡し、その結果を返します。
例
(define cont #f) (begin (+ 1 (call/cc (lambda (k) (set! cont k) 0))) (display "The number is: ") (write (cont 41)) (newline))
出力:
The number is: The number is: The number is: …
cont は call/cc が呼び出された時点から再開する手続きになります。
継続についてさらに
(define (displayln obj) (display obj) (newline)) (define cont #f) (displayln (call/cc (lambda (k) (set! cont k) "cont set"))) (begin (displayln "procedure called") (displayln "after procedure call") (cont "continuation called") (displayln "after continuation call"))
出力:
cont set procedure called after procedure call continuation called
通常の手続きを呼び出した後はそのまま実行が続きます。
cont を呼び出すと、プログラムは call/cc が実行された場所へジャンプします。
call/cc が手続きに渡す引数 k は「残りの計算」を表しており、これは Scheme の曖昧な選択演算子や複数戻り値の原理です。
Scheme で GOTO を実装する
以下は簡単なマクロです:
(define-syntax with-goto (syntax-rules () [(_ goto rest ...) (let () (define goto #f) (%labels rest ...) (call/cc (lambda (k) (set! goto (lambda (label) (k (label)))) rest ...)))])) (define-syntax %labels (syntax-rules () [(_) (begin)] [(_ (_ ...) rest ...) (%labels rest ...)] [(_ label rest ...) (begin (define (label) rest ...) (%labels rest ...))]))
使用例
(with-goto goto loop (display "Hello, world!\n") (goto loop))
「Hello, world!」が永遠に表示されます。
より複雑な例:
(let ([x 1]) (with-goto go (go loop) double (set! x (* 2 x)) loop (display x) (newline) (when (< x 1000) (go double)) (display "done\n")))
出力:
1 2 4 8 16 32 64 128 256 512 1024 done
動作の仕組み
は GOTO 手続き用の可変プレースホルダーを持つ新しいレキシカル環境を生成します。with-goto
は各ラベルをゼロ引数のサンク(その時点から残りの本体を実行する手続き)に変換します。%labels
の内部で、call/cc
を設定し、呼び出されたときに選択したラベルのサンクを継続に渡してジャンプします。goto
最後の実験
(with-goto go a (display "A") b (display "B") (go (if (zero? (random 2)) a b)))
実行前に何が起こるか予測してみてください。
プログラムはラベル
a と b の間を無限ループでジャンプしながら「A」または「B」を出力します。
結論
この実装は、継続がどれほど強力かつ危険であるかを示す単なるデモです。
call/cc でできることは数多くありますが、このマクロは実用的というより学習用の演習に近いものです。
さらに深く知りたい方は以下をご覧ください:
- Dybvig の The Scheme Programming Language
- 「Delimited Continuations」と ⁻Ƒ⁻ 演算子
読んでいただきありがとうございました!