正如@pad 用他的代码演示的那样,应该应用的逻辑是进行递归调用,在解决函数当前范围的问题之前递归地解决子问题。
在 的情况下largest
,向后解决它实际上没有任何意义,因为它只是使用更多的堆栈空间,这在“手动”执行代码时变得很明显。然而,设计模式在其他情况下很有用。想象一个名为的函数zip
:
(* zip ([1,2,3,4], [a,b,c]) = [(1,a),(2,b),(3,c)] *)
fun zip (x::xs, y::ys) = (x,y) :: zip (xs, ys)
| zip _ = []
此函数将两个列表的元组转换为许多元组的列表,丢弃剩余值。它在某些情况下可能很有用,并且定义它也不是那么糟糕。然而,制作它的对应物 ,unzip
有点棘手:
(* unzip ([(1,a),(2,b),(3,c)] = ([1,2,3],[a,b,c]) *)
fun unzip [] = ([], [])
| unzip ((x,y)::xys) =
let
val (xs, ys) = unzip xys
in
(x::xs, y::ys)
end
手动运行常规“从头开始” - 最大可能如下所示:
largest [1,4,2,3,6,5,4,6,7]
~> largest [4,2,3,6,5,4,6,7]
~> largest [4,3,6,5,4,6,7]
~> largest [4,6,5,4,6,7]
~> largest [6,5,4,6,7]
~> largest [6,4,6,7]
~> largest [6,6,7]
~> largest [6,7]
~> largest [7]
~> 7
手动运行“从头开始” - 最大,想象每个缩进级别都需要将函数调用的当前上下文保存在堆栈内存中,调用新函数并在~>
箭头之后返回结果,可能如下所示:
largest [1,4,2,3,6,5,4,6,7] ~> 7
\_
largest [4,2,3,6,5,4,6,7] ~> 7
\_
largest [2,3,6,5,4,6,7] ~> 7
\_
largest [3,6,5,4,6,7] ~> 7
\_
largest [6,5,4,6,7] ~> 7
\_
largest [5,4,6,7] ~> 7
\_
largest [4,6,7] ~> 7
\_
largest [6,7] ~> 7
\_
largest [7] ~> 7
那么,如果这些非尾递归函数只是使用更多内存,为什么它们会使早期递归调用有用呢?好吧,如果我们回过头来unzip
想在没有这种烦人的“反向思考”的情况下解决它,那么在构建新结果(元组)时就会遇到问题,因为我们没有任何地方可以放置 x 和 y:
(* Attempt 1 to solve unzip without abusing the call stack *)
fun unzip [] = ([], [])
| unzip ((x,y)::xys) = ...something with x, y and unzip xys...
制作这样一个地方的一个想法是创建一个辅助函数(辅助函数),它有一些额外的函数参数来构建xs
和ys
。当到达 xys 列表的末尾时,将返回这些值:
(* Attempt 2 to solve unzip without abusing the call stack *)
local
fun unzipAux ([], xs, ys) = (xs, ys)
| unzipAux ((x,y)::xys, xs, ys) = unzipAux (xys, x::xs, y::ys)
in
fun unzip xys = unzipAux (xys, [], [])
end
但是您可能已经意识到,(xs, ys)
结果最终会逆转。解决此问题的一种快速方法是rev
仅在它们上使用一次,这最好在基本情况下实现:
(* Attempt 3 to solve unzip without abusing the call stack *)
local
fun unzipAux ([], xs, ys) = (rev xs, rev ys)
| unzipAux ((x,y)::xys, xs, ys) = unzipAux (xys, x::xs, y::ys)
in
fun unzip xys = unzipAux (xys, [], [])
end
这就引出了一个有趣的问题:如何rev
实现?