在我尝试学习函数式编程时,我决定在haskell中实现代码挑战。
在进行挑战时https://adventofcode.com/2017/day/5 输入数据https://adventofcode.com/2017/day/5/input我遇到了几个问题。
这是我的代码
import Data.Array
import System.IO
listToArray l =
let n_elem = length l
pos_val = zip (range (0, n_elem)) l
in array (0, n_elem-1) pos_val
getData filename = do
s <- readFile filename
let l = map read (lines s) ::[Int]
a = listToArray l
return a
-- Part 1
updatePosArray i a =
let i_val = a ! i
in (i+i_val, a//[(i, i_val + 1)])
solution1 a n_steps i
| i >= length a || i < 0 = n_steps
| otherwise =
let ai = updatePosArray i a
in solution1 (snd ai) (n_steps+1) (fst ai)
-- Part 2
updatePosArray2 i a =
let i_val = a ! i
in
if i_val>=3 then (i+i_val, a//[(i, i_val-1)])
else (i+i_val, a//[(i, i_val+1)])
solution2 a n_steps i
| i >= length a || i < 0 = n_steps
| otherwise =
let ai = updatePosArray2 i a
in solution2 (snd ai) (n_steps+1) (fst ai)
main = do
x <- getData "/Users/lucapuggini/Documents/AdventOfCode/data/data_ch5_p1.txt"
let x_ex = array (0,4) [(0, 0), (1, 3), (2, 0), (3, 1), (4, -3)]
let n_steps_ex1 = solution1 x_ex 0 0
print $ n_steps_ex1
let n_steps1 = solution1 x 0 0
print $ n_steps1
let n_steps_ex2 = solution2 x_ex 0 0
print $ n_steps_ex2
-- very slow. Probably due to the immutable array
let n_steps2 = solution2 x 0 0
print $ n_steps2
这是我得到的结果:
lucas-MacBook-Pro:src lucapuggini$ stack runhaskell challenge5.hs
5
381680
10
stack overflow
代码安静缓慢,但这可能是由于我使用不可变数组这一事实,但我对堆栈溢出错误感到惊讶。我认为尾部递归不应该发生这种情况。
总之,我有两个问题:
1)为什么我收到stackoverflow错误?我错误地使用了尾递归吗?
2)运行此代码的更有效但功能更强的方法是什么?不可变数组是一个糟糕的选择吗?
我对haskell很新,所以请说清楚。
答案 0 :(得分:3)
关于你的第一个问题(为什么堆栈溢出):
使用stack runhaskell
(或等效stack runghc
)以特殊的“及时”编译模式运行代码,这与运行GHCi提示符时输入的表达式非常相似。代码未被优化,并且经常表现出糟糕的性能特征。
对于您的特定程序,这意味着它运行速度非常慢,内存占用不断扩大,最终会产生堆栈溢出。
如果您改为编译并运行:
stack ghc -- -O2 challenge5.hs
./challenge5
你会发现它运行得更快(在我的笔记本电脑上大约一分钟),在恒定的内存中,显然没有堆栈溢出。
如注释中所示,GHC中的堆栈溢出错误与尾递归实际上没有任何关系。相反,它源于懒惰评估的特定方面。 (例如,请参阅Do stack overflow errors occur in Haskell?。)
简而言之,GHC创建了代表未评估表达的“thunk”,其价值可以在未来的某个点被要求。有时,这些thunk以长链的形式链接在一起,当需要链的一端的thunk的值时,链中的所有thunks需要“部分评估”到链的末尾在程序开始计算thunk值之前,获取最后一个thunk的值。 GHC在有限大小的堆栈中维护所有这些“正在进行的thunk评估”,并且可以溢出。
触发堆栈溢出的一个简单示例是:
-- Sum.hs
main = print $ sum [1..100000000]
如果您运行:
stack runhaskell -- Sum.hs # using GHC version 8.0.2
你会得到:
Sum.hs: stack overflow
然而,使用ghc -O2
进行编译足以使问题消失(Sum.hs
和原始程序中的问题)。其中一个原因可能是采用“严格性分析”优化措施,这种优化措施可以尽早推动这些长链,从而无法形成这些长链。
关于你的第二个问题(不可变数组是正确的方法):
正如@WillNess指出的那样,使用未装箱的阵列取代盒装阵列可以带来巨大的性能提升:在我的笔记本电脑上,未装箱的代码版本在8秒内运行,而在63秒内运行。
然而,对于这种类型的算法 - 基本上,在对向量进行大量小的更改的情况下,要做出的更改取决于累积的先前更改的整个历史记录 - 您可以使用 mutable 数组做得更好。我有一个使用Data.Vector.Unboxed.Mutable
的版本,它在0.12秒内运行第2部分,你应该可以使用来自Data.Array.Unboxed
的可变无箱数组实现类似的性能。
答案 1 :(得分:1)
如果您导入Data.Array.Unboxed
而不是Data.Array
,并将您的数组声明为
....
a :: UArray Int Int
a = listToArray l
return a
或者其他什么,在getData
中,你会获得显着的加速,接近奇迹。
此外,您必须重新实施lengthArr = rangeSize . bounds
,因此它适用于未装箱的阵列。