継続の「実際の」使用例を探しています
-
09-06-2019 - |
質問
私は継続の概念を理解しようとしていますが、このような小さな教育例をいくつか見つけました。 ウィキペディアの記事:
(define the-continuation #f)
(define (test)
(let ((i 0))
; call/cc calls its first function argument, passing
; a continuation variable representing this point in
; the program as the argument to that function.
;
; In this case, the function argument assigns that
; continuation to the variable the-continuation.
;
(call/cc (lambda (k) (set! the-continuation k)))
;
; The next time the-continuation is called, we start here.
(set! i (+ i 1))
i))
この小さな関数が何をするのかは理解できましたが、それを明確に応用することができません。コード全体で継続をすぐに使用するつもりはありませんが、継続が適しているケースをいくつか知っていればと思います。
そこで、継続がプログラマーとして私に提供できるものを示す、より明示的に役立つコード サンプルを探しています。
乾杯!
解決
Algo & Data II では、(長い) 関数から「終了」または「戻る」ためにこれらを常に使用しました。
たとえば、ツリーを走査するための BFS アルゴリズムは次のように実装されました。
(define (BFS graph root-discovered node-discovered edge-discovered edge-bumped . nodes)
(define visited (make-vector (graph.order graph) #f))
(define q (queue.new))
(define exit ())
(define (BFS-tree node)
(if (node-discovered node)
(exit node))
(graph.map-edges
graph
node
(lambda (node2)
(cond ((not (vector-ref visited node2))
(when (edge-discovered node node2)
(vector-set! visited node2 #t)
(queue.enqueue! q node2)))
(else
(edge-bumped node node2)))))
(if (not (queue.empty? q))
(BFS-tree (queue.serve! q))))
(call-with-current-continuation
(lambda (my-future)
(set! exit my-future)
(cond ((null? nodes)
(graph.map-nodes
graph
(lambda (node)
(when (not (vector-ref visited node))
(vector-set! visited node #t)
(root-discovered node)
(BFS-tree node)))))
(else
(let loop-nodes
((node-list (car nodes)))
(vector-set! visited (car node-list) #t)
(root-discovered (car node-list))
(BFS-tree (car node-list))
(if (not (null? (cdr node-list)))
(loop-nodes (cdr node-list)))))))))
ご覧のとおり、node-discovered 関数が true を返すとアルゴリズムは終了します。
(if (node-discovered node)
(exit node))
この関数は「戻り値」も返します。'ノード'
関数が終了する理由は、次のステートメントによるものです。
(call-with-current-continuation
(lambda (my-future)
(set! exit my-future)
exit を使用すると、実行前の状態に戻り、呼び出しスタックを空にして、指定した値を返します。
したがって、基本的に、call-cc は、再帰全体が自動的に終了するのを待つのではなく、再帰関数からジャンプするために (ここで) 使用されます (大量の計算作業を行う場合、非常にコストがかかる可能性があります)。
call-cc で同じことを行う別の小さな例:
(define (connected? g node1 node2)
(define visited (make-vector (graph.order g) #f))
(define return ())
(define (connected-rec x y)
(if (eq? x y)
(return #t))
(vector-set! visited x #t)
(graph.map-edges g
x
(lambda (t)
(if (not (vector-ref visited t))
(connected-rec t y)))))
(call-with-current-continuation
(lambda (future)
(set! return future)
(connected-rec node1 node2)
(return #f))))
他のヒント
シーサイド:
@パット
シーサイド
はい、 シーサイド は素晴らしい例です。そのコードをざっと閲覧したところ、Web 上で一見ステートフルな方法でコンポーネント間で制御を渡すことを示すこのメッセージを見つけました。
WAComponent >> call: aComponent
"Pass control from the receiver to aComponent. The receiver will be
temporarily replaced with aComponent. Code can return from here later
on by sending #answer: to aComponent."
^ AnswerContinuation currentDo: [ :cc |
self show: aComponent onAnswer: cc.
WARenderNotification raiseSignal ]
とても素敵です!
私は独自の単体テスト ソフトウェアを構築しました。テストを実行する前に継続を保存し、失敗した場合は (オプションで) スキーム インタープリタにデバッグ モードに移行し、継続を再度呼び出すように指示します。こうすることで、問題のあるコードを非常に簡単にステップ実行できます。
継続がシリアル化可能な場合は、アプリケーションの失敗時に保存し、それらを再呼び出しして、変数値、スタック トレースなどに関する詳細情報を取得することもできます。
継続は、セッション情報を保存するために一部の Web サーバーおよび Web フレームワークによって使用されます。継続オブジェクトはセッションごとに作成され、セッション内の各リクエストで使用されます。
の実装に遭遇しました amb
の演算子 この郵便受け から http://www.randomhacks.net, 、継続を使用します。
これがその内容です amb
オペレーターは次のことを行います:
# amb will (appear to) choose values
# for x and y that prevent future
# trouble.
x = amb 1, 2, 3
y = amb 4, 5, 6
# Ooops! If x*y isn't 8, amb would
# get angry. You wouldn't like
# amb when it's angry.
amb if x*y != 8
# Sure enough, x is 2 and y is 4.
puts x, y
そして、投稿の実装は次のとおりです。
# A list of places we can "rewind" to
# if we encounter amb with no
# arguments.
$backtrack_points = []
# Rewind to our most recent backtrack
# point.
def backtrack
if $backtrack_points.empty?
raise "Can't backtrack"
else
$backtrack_points.pop.call
end
end
# Recursive implementation of the
# amb operator.
def amb *choices
# Fail if we have no arguments.
backtrack if choices.empty?
callcc {|cc|
# cc contains the "current
# continuation". When called,
# it will make the program
# rewind to the end of this block.
$backtrack_points.push cc
# Return our first argument.
return choices[0]
}
# We only get here if we backtrack
# using the stored value of cc,
# above. We call amb recursively
# with the arguments we didn't use.
amb *choices[1...choices.length]
end
# Backtracking beyond a call to cut
# is strictly forbidden.
def cut
$backtrack_points = []
end
好き amb
!
継続は、プログラム フローが直線的でない場合や、事前に決定されていない場合でも、「実際の」例で使用できます。よくある状況としては、 ウェブアプリケーション.
継続は、サーバー プログラミング (Web アプリケーション フロントエンドを含む) におけるリクエストごとのスレッドの良い代替手段です。
このモデルでは、リクエストが届くたびに新しい (重い) スレッドを起動するのではなく、関数内で何らかの作業を開始するだけです。次に、I/O でブロックする準備ができたら (つまり、データベースからの読み取り)、ネットワーク応答ハンドラーに継続を渡します。応答が返されたら、継続を実行します。このスキームを使用すると、わずか数個のスレッドで大量のリクエストを処理できます。
これにより、ブロック スレッドを使用するよりも制御フローが複雑になりますが、負荷が高い場合は (少なくとも今日のハードウェアでは) 効率的になります。
amb 演算子は、プロローグのような宣言型プログラミングを可能にする良い例です。
私が話している間、私は Scheme で音楽作曲ソフトウェアをコーディングしています (私はミュージシャンで、音楽の背後にある理論についての知識はほとんどなく、背後にある数学がどのように機能するかを確認するために自分の作品を分析しているだけです)。
amb 演算子を使用すると、メロディーが満たさなければならない制約を入力するだけで、Scheme に結果を判断させることができます。
おそらく言語哲学のために継続が Scheme に入れられているのでしょう。Scheme は、Scheme 自体でライブラリを定義することで、他の言語にあるあらゆるプログラミング パラダイムを実現できるようにするフレームワークです。継続は、「return」、「break」などの独自の抽象制御構造を作成したり、宣言型プログラミングを有効にしたりするために使用されます。Scheme はより「一般化」されており、そのような構成要素をプログラマが指定できるようにする必要があります。
どうですか? Google マップレット API?たくさんの関数があります (すべて末尾が Async
) にコールバックを渡します。API 関数は非同期リクエストを実行し、その結果を取得し、その結果を (「次に行うこと」として) コールバックに渡します。とても似ていますね 継続パススタイル 私に。
これ 例 は非常に単純なケースを示しています。
map.getZoomAsync(function(zoom) {
alert("Current zoom level is " + zoom); // this is the continuation
});
alert("This might happen before or after you see the zoom level message");
これはJavaScriptなので、 テールコールの最適化, したがって、継続を呼び出すたびにスタックが増大し、最終的には制御のスレッドがブラウザに返されることになります。いずれにしても、これは素晴らしい抽象化だと思います。
非同期アクションを呼び出し、結果を取得するまで実行を一時停止する必要がある場合は、通常、結果をポーリングするか、コードの残りの部分をコールバックに入れて、完了時に非同期アクションによって実行されます。継続を使用すると、ポーリングという非効率なオプションを実行する必要がなく、非同期イベントの後に実行されるすべてのコードをコールバックでまとめる必要もありません。コードの現在の状態をコールバックとして渡すだけです。 - そして、非同期アクションが完了するとすぐに、コードは効果的に「ウェイクアップ」されます。
継続は例外やデバッガを実装するために使用できます。