在具有可变状态的命令式/面向对象编程中,声明如下结构是非常常见和有用的:
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,但我不确定这是否是表达这种依赖属性的懒惰评估的好方法,因为它似乎是一个复杂的解决方案。如何存储衍生价值而不必重新计算它们?
答案 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
内。当inverseMass
为Just 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是懒惰的,所以只有在需要时才会首次计算这些字段。