10

Xbox での開発用に、F# 仕様からの 最終的なワークフローの修正バージョンを使用しています。Xbox の .net フレームワークはテール コールをサポートしていないようです。このため、コンパイル時に末尾呼び出しの最適化を無効にする必要があります。

最初は、この制限が計算式でのあらゆる形式のループの使用を防止するように思われるかもしれませんが、私は当初、「ステッピング」によってその問題を回避できると考えていました。 f を呼び出すラムダを含む 最終的に値を返します。

実験の結果、while ループ (計算式で使用してもスタック オーバーフローは発生しません) については正しかったが、再帰関数についてはそうではありませんでした。

明確にするために、これは機能します:

// Wait until "start" or "A" is pressed on one of the gamepads.
// Set "player" when that happens.
let player : PlayerIndex option ref = ref None
while (!player).IsNone do
    for p in all_players do
        let state = GamePad.GetState(p)
        if state.IsConnected
            && (state.Buttons.Start = ButtonState.Pressed
                || state.Buttons.A = ButtonState.Pressed) then
            player := Some p
    do! sys.WaitNextFrame()

これにより、スタック オーバーフローが発生します。

// Wait until "start" is pressed on the controlling gamepad.
let rec wait() = task {
    input.Update()
    if not (input.IsStartPressed()) then
        do! sys.WaitNextFrame()
        do! wait()
}

2 番目のケースでは、スタック トレースは "bind@17-1" への長い一連の呼び出しを示しています。そのコードを以下に示します。スタック トレースに表示される行番号は 17 行目です。

let rec bind k e =
    match e with
    | Completed r ->
        Running(fun () -> k r)
    | Running f ->
        Running(fun () -> f() |> bind k)  // line 17
    | Blocked(dt, f) ->
        Blocked(dt, fun () -> f() |> bind k)
    | BlockedNextFrame f ->
        BlockedNextFrame(fun () -> f() |> bind k)
    | Yield f ->
        Yield(fun () -> f() |> bind k)

どこが間違っていますか?「ステップ可能な再帰」がスタックオーバーフローに対して無害であるという私の推論は間違っていますか? バインドの実装に何か問題がありますか?

ああ、私の頭!再帰を伴う継続渡しは頭痛の種です...

編集:「スタックオーバーフローに関して無害なステップ可能な再帰」という名前が付けられました。いわゆるトランポリンです。

4

2 に答える 2

11

最後のdoを交換してください!リターン付き!:

// Wait until "start" is pressed on the controlling gamepad.
let rec wait() = task {
    input.Update()
    if not (input.IsStartPressed()) then
       do! sys.WaitNextFrame()
       return! wait()
}

編集

F# 仕様に従って、実行してください。wait()は Bind(wait(), fun() -> Zero()) に変換されるため、再帰呼び出しごとに Bind ネストのレベルが上がります (スタックトレースで確認できるように)。

に!wait()は、次のステップで消費できる新しい「最終的に」計算をすぐに返します。

于 2011-02-04T17:27:07.600 に答える
6

何が起こっているのかを理解する 1 つの方法は、型シグネチャを調べることです。

type TaskBuilder() =
    // do!
    // Eventually<'a> * ('a -> Eventually<'b>) -> Eventually<'b>
    member x.Bind(e, f) = bind f e

    // return!
    // Eventually<'a> -> Eventually<'a>
    member x.ReturnFrom(r : Eventually<_>) = r

    // return
    // 'a -> Eventually<'a>
    member x.Return(r) = result r


let result r = Completed(r)

f# のすべての関数は、何かを返す必要があります。したがって、次のコード

let rec wait() = task {
    input.Update()
    if not (input.IsStartPressed()) then
        do! sys.WaitNextFrame()
        do! wait()
}

と同等です

let rec wait() = task {
    input.Update()
    if not (input.IsStartPressed()) then
        do! sys.WaitNextFrame()
        do! wait()
        return ()
}

Return の定義を見ると、結果が呼び出され、結果が Completed(r) を返します。

タスク用に2つの小さなテストを作成しました。

let test7() =
    let sch = new Scheduler()
    let sys = new Environment(sch)

    let rec hop i = task {
        printfn "%i: Hop!" i
        //do! sys.Wait(1.0f)
        if i > 0 then
            do! hop (i - 1)
            return ()
    }

    runAllFixed sch 0.1f [| hop 3 |]

let test8() =
    let sch = new Scheduler()
    let sys = new Environment(sch)

    let rec hop i = task {
        printfn "%i: Hop!" i
        //do! sys.Wait(1.0f)
        if i > 0 then
            return! hop (i - 1)
    }

    runAllFixed sch 0.1f [| hop 3 |]

test7()
printfn "\n" 
test8()

いくつかのインストルメンテーションを使用すると、印刷されます。

Delay 3: Hop!
Delay Bind Running 2: Hop!
Delay Bind Running Running 1: Hop!
Delay Bind Running Running Running 0: Hop!
Zero Completed Running Running Return Completed Running Return Completed Return


Delay 3: Hop!
Delay ReturnFrom 2: Hop!
Delay ReturnFrom 1: Hop!
Delay ReturnFrom 0: Hop!
Zero

Computation Expression呼び出しに関する MSDN ドキュメント。

于 2011-02-05T03:52:17.463 に答える