2010-11-06 93 views
3

在下面的函數中,我試圖通過使用累加器來設置尾遞歸。但是,我遇到了堆棧溢出異常,這導致我相信我設置函數的方式不能正確啓用尾部遞歸。F#使用累加器,仍然出現堆棧溢出異常

//F# attempting to make a tail recursive call via accumulator 
let rec calc acc startNum = 
    match startNum with 
    | d when d = 1  -> List.rev (d::acc) 
    | e when e%2 = 0 -> calc (e::acc) (e/2) 
    | _     -> calc (startNum::acc) (startNum * 3 + 1) 

這是我的理解是使用acc讓編譯器,看看有沒有必要繼續圍繞每一個遞歸調用所有的棧幀,因爲它可以東西,在ACC每遍的結果,每幀返回。顯然,我不明白如何正確使用累加器值,因此編譯器會調用尾調用。

+3

如果在單聲道這樣做,你的運氣了,因爲單不優化尾調用的是,即使是F#。 – 2010-11-06 01:30:01

+0

我沒有Mono系統來檢查它,但是在Windows .NET下查看反射輸出顯示tailcall優化* *已經針對該版本進行了優化。 – TechNeilogy 2010-11-06 01:37:46

+6

你的函數是正確的尾遞歸。如果您使用Visual Studio並在調試模式下編譯,尾遞歸將默認關閉(以啓用更好的調試體驗):http://stackoverflow.com/questions/1416415/f-stackoverflow-project-euler-4 – 2010-11-06 01:39:24

回答

3

Stephen Swensen正確地指出,如果你調試,VS必須禁用尾部調用(否則它不會有棧幀跟隨調用堆棧)。我知道 VS做到了這一點,但只是簡單地忘記了。

得到這個之後,我想知道運行時或編譯器是否可以拋出一個更好的異常,因爲編譯器知道你在調試並且你寫了一個遞歸函數,在我看來它可能是可能它給你一個提示,如

'Stack Overflow Exception: a recursive function does not 
tail call by default when in debug mode' 
+1

「我知道VS做到了這一點,但只是簡單地忘記了」。是的,我覺得它真的很煩人。 – 2010-11-18 14:39:29

1

確實出現,這與.NET Framework 4的公告編譯時正確地得到轉換成尾調用,在反射器將其轉換你的函數爲while(true)你會期望在F#尾巴的功能做:

[CompilationArgumentCounts(new int[] { 1, 1 })] 
public static FSharpList<int> calc(FSharpList<int> acc, int startNum) 
{ 
    while (true) 
    { 
     int num = startNum; 
     switch (num) 
     { 
      case 1: 
      { 
       int d = num; 
       return ListModule.Reverse<int>(FSharpList<int>.Cons(d, acc)); 
      } 
     } 
     int e = num; 
     if ((e % 2) == 0) 
     { 
      int e = num; 
      startNum = e/2; 
      acc = FSharpList<int>.Cons(e, acc); 
     } 
     else 
     { 
      startNum = (startNum * 3) + 1; 
      acc = FSharpList<int>.Cons(startNum, acc); 
     } 
    } 
} 

您的問題未從缺乏制止它是一個尾調用(如果您使用F#2.0我不知道結果會是什麼)。你究竟如何使用這個功能? (輸入參數)一旦我更好地瞭解該功能的功能,我可以更新我的答案,希望能夠解決它。