我试图在haskell中转换一些递归函数。为了获得有关此类函数的一些经验,我尝试了解尾递归的概念。为了获得线索,我想从非常简单的功能开始,以理解尾递归背后的概念。以下代码显示了我编写的随机递归函数。我想将其转换为尾递归变量,但在实际代码上使用理论概念时遇到问题。
h x = if x > 20 then 50 else x*x + h (x+1)
答案 0 :(得分:3)
正如罗宾·齐格蒙德(Robin Zigmond)所言,尾递归的概念在Haskell中的应用方式与非懒惰语言中的应用方式不同。在具有 non-lazy 语义(而不是Haskell)的语言中,可以实现尾部递归的方法是将导致堆栈使用的表达式移到一个累加参数上,如下所示:
h :: Int -> Int
h x = if x > 20 then 50 else x*x + h (x+1)
g :: Int -> Int
g z = g' z 50 where
g' x y
| x > 20 = y
| otherwise = g' (x+1) (x*x + y)
这里g'
的函数体的外部表达式是对它本身的调用,因此,如果这是一种非惰性语言,则在解决该问题之前,无需保留旧的递归调用的堆栈框架表达式的x*x + ...
部分。不过,在Haskell中,此结果会有所不同。
在微基准测试中比较您的h
和这个g
,
module Main where
import Criterion
import Criterion.Main
main :: IO ()
main = defaultMain [ bgroup "tail-recursion" [ bench "h" $ nf h 1
, bench "g" $ nf g 1
]
]
实际上,您从此g'
获得的性能更差:
benchmarking tail-recursion/h
time 826.7 ns (819.1 ns .. 834.7 ns)
0.993 R² (0.988 R² .. 0.997 R²)
mean 911.1 ns (866.4 ns .. 971.9 ns)
std dev 197.7 ns (149.3 ns .. 241.3 ns)
benchmarking tail-recursion/g
time 1.742 μs (1.730 μs .. 1.752 μs)
1.000 R² (0.999 R² .. 1.000 R²)
mean 1.742 μs (1.729 μs .. 1.758 μs)
std dev 47.44 ns (34.69 ns .. 66.29 ns)
通过严格设置g'
的参数,您可以获得一些性能,
{-# LANGUAGE BangPatterns #-}
g2 :: Int -> Int
g2 z = g' z 50 where
g' !x !y
| x > 20 = y
| otherwise = g' (x+1) (x*x + y)
但它的外观和性能都比原始h
差:
benchmarking tail-recursion/g2
time 1.340 μs (1.333 μs .. 1.349 μs)
1.000 R² (0.999 R² .. 1.000 R²)
mean 1.344 μs (1.336 μs .. 1.355 μs)
std dev 33.40 ns (24.71 ns .. 48.94 ns)
编辑:正如K. A. Buhr所指出的,我忘记了GHC的-O2
标志;这样做可以提供以下微基准测试结果:
h time: 54.27 ns (48.05 ns .. 61.24 ns)
g time: 24.50 ns (21.15 ns .. 27.35 ns)
g2 time: 25.47 ns (22.19 ns .. 29.06 ns)
这时,累积参数版本的性能确实更好,而BangPatterns
版本的性能也很好,但是两者看上去都比原始版本差。
因此,通常在尝试优化代码时要遵循的道德准则:不要过早地这样做。尤其是在尝试优化Haskell代码时,有一个道理:在尝试之前,您不一定会知道它很重要,通常,最依赖库函数的抽象解决方案效果很好。