首次访问时,Haskell仅评估一次数据类型的属性?

时间:2017-09-30 21:32:26

标签: haskell functional-programming lazy-evaluation

在具有可变状态的命令式/面向对象编程中,声明如下结构是非常常见和有用的:

var s = n?n<0?-1:1:0;

来源:http://allenchou.net/2013/12/game-physics-motion-dynamics-implementations/

此处有许多属性可以直接从其他属性计算,例如来自struct RigidBody { float m_mass; float m_inverseMass; Mat3 m_localInverseInertiaTensor; Mat3 m_globalInverseInertiaTensor; Vec3 m_globalCentroid; Vec3 m_localCentroid; Vec3 m_position; Mat3 m_orientation; Vec3 m_linearVelocity; Vec3 m_angularVelocity; }; 的{​​{1}}。在像Haskell这样的无状态编程语言中,获取派生值很容易:

m_inverseMass

但这会在我们每次需要时计算m_mass,这可能会变得很昂贵,特别是在性能至关重要的领域,如物理模拟。我考虑过memoization,但我不确定这是否是表达这种依赖属性的懒惰评估的好方法,因为它似乎是一个复杂的解决方案。如何存储衍生价值而不必重新计算它们?

2 个答案:

答案 0 :(得分:11)

正如@ 4castle和@Shersh所说,一个简单的方法是将派生值包含在数据类型中:

data RigidBody = RigidBody
  { m_mass :: Float
  , m_inverseMass :: Float }

然后使用智能构造函数创建新的RigidBody s:

rigidBody mass = RigidBody mass (1/mass)

表达式1/mass将为m_inverseMass创建一个thunk,在首次评估之后,它将在不重新计算的情况下可用,因此它提供了一种自动记忆。

更一般的转换,例如更改位置并根据global*值正确更新所有local*字段,将以类似的方式处理。作为简化示例:

module Rigid where

type Vec3 = Double  -- just to type check

data RigidBody = RigidBody
  { m_mass :: Float
  , m_inverseMass :: Float
  , m_pos :: Vec3
  , m_localCentroid :: Vec3
  , m_globalCentroid :: Vec3
  }

rigidBody mass pos centroid =
  RigidBody mass (1/mass) pos centroid (centroid + pos)

move body delta =
  rigidBody (m_mass body)
            (m_pos body + delta)
            (m_localCentroid body)

在性能至关重要的应用程序中,您可能希望采取措施在适当的位置引入严格性,这样您就不会积累大量无价值的thunk。

答案 1 :(得分:0)

您可以将inverseMass Maybe Float存储在RigidBody内。当inverseMassJust someMass时,您只需提取此值即可。如果是Nothing,则计算它并存储在RigidBody内。问题出在这个商店部分。因为您可能知道对象在Haskell中是不可变的。

天真但简单的解决方案是在每次计算之后返回RigidBody

data RigidBody = RigidBody 
    { rigidBodyMass        :: Float
    , rigidBodyInverseMass :: Maybe Float }

inverseMass :: RigidBody -> (Float, RigidBody)
inverseMass b@(RigidBody _ (Just inv)) = (inv, b)
inverseMass   (RigidBody mass Nothing) = let inv = 1 / mass 
                                         in (inv, RigidBody mass (Just inv))

如果你有很多这样的领域,你会发现这种方法非常繁琐。使用这些函数编写代码并不是很方便。所以这里是State monad变得方便的地方。 State monad可以将当前RigidBody保持在显式状态,并通过所有有状态计算相应地更新它。像这样:

inverseMass :: State RigidBody Float
inverseMass = do
    RigitBody inv maybeInverse <- get
    case maybeInverse of
        Just inv -> pure inv
        Nothing  -> do
            let inv = 1 / mass
            put $ RigidBody mass (Just inv)
            pure inv

稍后您可以多次使用inverseMass,并且只有在您第一次调用时才会计算质量的倒数。

你会发现,在命令式编程语言中,像C ++这样的语言是明确的。您想要更新RigidBody的字段。所以基本上你有一些RigidBody类型的对象存储一些状态。由于state是隐式的,因此您无需在函数中指定它们更改RigidBody的字段。在Haskell(以及所有优秀的编程语言)中,您明确指定了您的状态以及如何更改它。您明确指定要使用的对象。 inverseMass monadic动作(或者只是你想要的功能)将根据调用此函数时的当前状态更新显式状态。对于这类任务,这或多或少都是Haskell中惯用的方法。

嗯,另一个惯用解决方案:只需创建数据类型的值,并将所有字段设置为某些函数调用。因为Haskell是懒惰的,所以只有在需要时才会首次计算这些字段。