8

最近はF#を勉強中。私はさまざまな方法で問題を解決しようとします。このような:

(*

[0;1;2;3;4;5;6;7;8] -> [(0,1,2);(3,4,5);(6,7,8)]

*)

//head-recursive
let rec toTriplet_v1 list=
    match list with
    | a::b::c::t -> (a,b,c)::(toTriplet_v1 t) 
    | _ -> []

//tail-recursive
let toTriplet_v2 list=
    let rec loop lst acc=
        match lst with
        | a::b::c::t -> loop t ((a,b,c)::acc)
        | _ -> acc
    loop list []

//tail-recursive(???)
let toTriplet_v3 list=
    let rec loop lst accfun=
        match lst with
        | a::b::c::t -> loop t (fun ls -> accfun ((a,b,c)::ls))
        | _ -> accfun []
    loop list (fun x -> x)

let funs = [toTriplet_v1; toTriplet_v2; toTriplet_v3];
funs |> List.map (fun x -> x [0..8]) |> List.iteri (fun i x -> printfn "V%d : %A" (i+1) x)

V2 と V3 の結果は同じはずだと思いました。しかし、私は以下の結果を得ます:

V1 : [(0, 1, 2); (3, 4, 5); (6, 7, 8)]
V2 : [(6, 7, 8); (3, 4, 5); (0, 1, 2)]
V3 : [(0, 1, 2); (3, 4, 5); (6, 7, 8)]

V2 と V3 の結果が異なるのはなぜですか?

4

1 に答える 1

11

V2 は標準の累積変数を使用して末尾再帰を実行します。

loop ([0;1;2;3;4;5;6;7;8], []) ->
  loop ([3;4;5;6;7;8], [(0,1,2)]) ->
    loop ([6;7;8], [(3,4,5), (0,1,2)]) ->
      loop ([], [(6,7,8), (3,4,5), (0,1,2)]) ->
        [(6,7,8), (3,4,5), (0,1,2)]

V3 はcontinuation、または平易な英語で言えば、累積関数を使用します。

loop ([0;1;2;3;4;5;6;7;8], x->x) ->
  loop ([3;4;5;6;7;8], x->(0;1;2)::x) ->
    loop ([6;7;8], x->(3;4;5)::x) ->
      loop ([], x->(6,7,8)::x) ->
        [(6,7,8)]  // x->(6,7,8)::x applies to []
    ->
      [(3,4,5);(6,7,8)] // x->(3,4,5)::x applies to [(6,7,8)]
  ->
    [(0,1,2);(3,4,5);(6,7,8)] // x->(0,1,2)::x applies to [(3,4,5);(6,7,8)]

変数の累積と関数の累積の違いを確認できます。

累積変数の使用は、累積変数が答えを格納するため、最後の呼び出しで停止します。ただし、累積関数は、最後の呼び出しの後もバックトラック作業を行います。loop t (fun ls -> accfun ((a,b,c)::ls))再帰呼び出しは実際にはこの関数の最後のステートメントである ため、累積関数の使用は実際には末尾再帰であることに注意してください。

ところで、あなたが提供したコードは、概念の末尾再帰関数を示す良い例です。これらのサンプル コードを理解する 1 つの方法は、上の 2 つの図で行ったように、小さなケースで作業することです。いくつかの小さなケースに取り組んだ後、概念をより深く理解できます。

于 2010-06-01T08:50:59.707 に答える