这个递归柯里化函数有什么问题

What's wrong with this recursive curried function

我正在尝试编写一个函数来解决以下问题;

persistence 39 = 3  // because 3*9 = 27, 2*7 = 14, 1*4=4
                    // and 4 has only one digit

persistence 999 = 4 // because 9*9*9 = 729, 7*2*9 = 126,
                    // 1*2*6 = 12, and finally 1*2 = 2

persistence 4 = 0   // because 4 is already a one-digit number

在我解决了这个问题后,我试图让所有函数看起来像这样 Ramda.js 函数样式; 此代码有效;

let multiply = List.reduce (*)

let gt from input = input > from

let just input = fun _ -> input

let ifElse cond trueFn falseFn input =
  if cond input then trueFn input else falseFn input

let digits n =
  (string n) |> Seq.toList |> List.map (System.Char.GetNumericValue >> int)
  
let rec persRec iter current =
  current 
  |> digits 
  |> multiply
  |> ifElse (gt 9) (persRec (iter + 1)) (just iter)

let persistence n = if n > 9 then persRec 1 n else 0

但是当我尝试使用像下面这样的柯里化组合版本修改 persRec 函数时,它会导致堆栈溢出。

let rec persRec iter = 
  digits 
  >> multiply
  >> ifElse (gt 9) (persRec (iter + 1)) (just iter)

这有什么问题?

函数 persRec 正在无条件调用自身。这里:

>> ifElse (gt 9) (persRec (iter + 1)) (just iter)
                 ^^^^^^^^^^^^^^^^^^^^
                           |
                           unconditional recursive call

这种情况经常发生。每次persRec被人调用时,它会立即调用自己。

您可能希望递归调用只在 gt 9 时发生,因为毕竟它在 ifElse 中,对吧?但这没关系:ifElse 并不特殊,它只是一个函数。为了调用函数,F# 必须在调用 之前计算其所有参数 (也称为“求值的应用顺序”),这意味着它必须在它之前调用 persRec (iter + 1)可以调用 ifElse,它必须在调用 (>>) 之前调用 ifElse,并且必须调用 (>>) 才能计算 persRec 的结果。所以最终,它需要调用 persRec 来计算 persRec 的结果。看看这是怎么回事?

之前的版本有效,因为 persRec 的主体在调用 ifElse 之前并未实际执行。 persRec 的主体只有在其所有参数都提供时才会执行,而最后一个参数只有在条件为真时才会在 ifElse 的主体内提供。


在我看来,混淆源于指称语义和操作语义之间的差异。是的,在数学上、逻辑上,函数是等价的。但执行也很重要。正常与应用评估顺序。内存问题。表现。这些都在 lambda 演算的范围之外。