在Haskell

时间:2016-03-14 21:20:29

标签: haskell memory recursion

昨天我终于决定开始学习Haskell。我开始扫描教程,但很快就决定实际练习会更有益。因此,我继续移植我的python脚本,据说可以模拟重力进入Haskell。令我惊讶的是它确实有效,生成的值与python的值相匹配。

我意识到实施可能非常糟糕。可怕的性能缺乏并没有给我带来太大的麻烦,但令我困扰的是,当我试图长时间运行模拟时,我的内存不足。这是因为实施本质上存在缺陷还是可以使其发挥作用?

我尝试用三种不同的方法构造主循环:&#34;迭代&#34;,一个递归函数(我读过关于尾递归,但没有设法让它工作)和一个更多实验性递归功能。相关功能名为 模拟 测试 test2 < / strong>即可。我用选项&#34; -O2&#34;。

编译了程序

为什么程序内存不足,我该怎么做才能防止这种情况发生?

代码不是那么相关的部分:

import System.Environment
import Data.List (tails)
import System.CPUTime
import Text.Printf
import Control.Exception

gConst = 6.674e-11

data Vector = Vector2D Double Double | Vector3D Double Double Double deriving (Show)
deltaVector :: Vector -> Vector -> Vector
deltaVector (Vector2D x1 y1) (Vector2D x2 y2) = Vector2D (x2 - x1) (y2 - y1)
deltaVector (Vector3D x1 y1 z1) (Vector3D x2 y2 z2) = Vector3D (x2 - x1) (y2 - y1) (z2 - z1)

data Position = Position Vector deriving (Show)
data Velocity = Velocity Vector deriving (Show)

distance2DSquared (Vector2D deltaX deltaY) = deltaX ** 2 + deltaY ** 2
distance3DSquared (Vector3D deltaX deltaY deltaZ) = (distance2DSquared $ Vector2D deltaX deltaY) + deltaZ ** 2
distance vector = sqrt (distance3DSquared $ vector)

force vector mass1 mass2 = gConst * (mass1 * mass2) / (distance3DSquared vector)
acceleration force mass = force / mass

vectorComponentDivide (Vector2D x y) c = Vector2D (x/c) (y/c)
vectorComponentDivide (Vector3D x y z) c = Vector3D (x/c) (y/c) (z/c)

vectorComponentMultiply (Vector2D x y) c = Vector2D (x*c) (y*c)
vectorComponentMultiply (Vector3D x y z) c = Vector3D (x*c) (y*c) (z*c)

vectorComponentAdd (Vector2D x1 y1) (Vector2D x2 y2) = Vector2D (x1+x2) (y1+y2)
vectorComponentAdd (Vector3D x1 y1 z1) (Vector3D x2 y2 z2) = Vector3D (x1+x2) (y1+y2) (z1+z2)

invertedVector (Vector2D x1 y1) = Vector2D (-x1) (-y1)
invertedVector (Vector3D x1 y1 z1) = Vector3D (-x1) (-y1) (-z1)

normalizedVector :: Vector -> Vector
normalizedVector vector = vectorComponentDivide vector $ distance vector

velocity vel0 mass1 mass2 vector deltaT = 
    vectorComponentMultiply (vectorComponentAdd vel0 (vectorComponentMultiply (normalizedVector vector) (acceleration (force vector mass1 mass2) mass1))) deltaT

data Actor = Actor String Vector Vector Double deriving (Show)

earth = Actor "Object1" (Vector3D 0 0 0) (Vector3D 0 0 0) 10
moon = Actor "Object2" (Vector3D 10 0 0) (Vector3D 0 0 0) 10

actors = [earth, moon]

combinations :: Int -> [a] -> [[a]]
combinations 0 _  = [ [] ]
combinations n xs = [ y:ys | y:xs' <- tails xs
                           , ys <- combinations (n-1) xs']

updateVelocity [(Actor name1 position1 velocity1 mass1),(Actor name2 position2 velocity2 mass2)] = 
    [(Actor name1 position1 a mass1),(Actor name2 position2 b mass2)]
    where a = velocity velocity1 mass1 mass2 vector deltaT
          b = velocity velocity2 mass2 mass1 (invertedVector vector) deltaT
          vector = deltaVector position1 position2
          deltaT = 1

updatePosition [(Actor name1 position1 velocity1 mass1),(Actor name2 position2 velocity2 mass2)] =
        [Actor name1 (vectorComponentAdd position1 velocity1) velocity1 mass1, Actor name2 (vectorComponentAdd position2 velocity2) velocity2 mass2]

相关部分:

update list = map updatePosition (map updateVelocity list)


simulation state n = do
    if n == 0
        then do
            print state
            return ()
        else do
            let newState = update state
            simulation newState $! (n-1)

test list n = iterate update list !! n

test2 list 0 = list
test2 list n = (test2 (update list) (n-1))

time :: IO t -> IO t
time a = do
    start <- getCPUTime
    v <- a
    end   <- getCPUTime
    let diff = (fromIntegral (end - start)) / (10^12)
    printf "Computation time: %0.3f sec\n" (diff :: Double)
    return v

main :: IO ()
main = do
    let combo = combinations 2 actors
    putStrLn "Hello World!"
    let n = 1000000
    print n
    --time $ print (test combo n)
    time $ simulation combo n
    _ <- getLine
    putStrLn "BAI"

1 个答案:

答案 0 :(得分:3)

我认为懒惰会损害您的代码:您的代码会构建大量的thunk(未评估的表达式),从而导致OOM。

例如,当您在中间访问结果列表而没有强制之前的列表元素时,iterate以导致大型thunk而闻名。更确切地说是

iterate f x !! n

很糟糕,因为它会在真正执行任何工作之前构建表达式f (f (f ...(f x)))。我们想在访问下一个列表元素之前评估每个列表元素。这可以通过自定义!!函数进行圆顶化:

(!!!) :: [a] -> Int -> a
[]     !!! _ = error "!!!: out of range"
(x:_ ) !!! 0 = x
(x:xs) !!! n = seq x (xs !!! pred n)

现在我们可以使用iterate f a !!! n而不会产生大的暴力。

这有同样的问题:

simulation state n = do
    if n == 0
        then do
            print state
            return ()
        else do
            let newState = update state
            simulation newState $! (n-1)

它将构建大型update (update (update ...)) thunks而不评估它们。可能的修复方法可能是

            ...
            (simulation $! newState) $! (n-1)

但是,请记住,在您的情况下,newState是一个列表(列表!)。在这种情况下,seq$!将仅要求对列表进行评估,直到其第一个单元格构造函数为止 - 足以检查列表是否为空。这个&#34;强迫&#34;可能已经足够或不适合您的目的。

有一个名为deepSeq的库函数会强制显示完整列表(如果确实需要)(使用Hoogle查找文档)。

总结:懒惰的评估有其好处和缺点。它通常允许更高的效率,例如有时提供恒定的空间列表处理,而无需编写精心设计的功能。它还允许使用无限的列表技巧。但是,它也可能导致不必要的thunk长时间停留,浪费内存。因此,在这些情况下,程序员会有一些负担。特别是当一个习惯于严格的语义时,这些问题一开始可能会让人感到恐惧(我们已经在那里!)。