Haskell 首次访问时仅评估数据类型的属性一次?
Haskell evaluating properties of a data type only once when first accessed?
在imperative/object面向可变状态的编程中,声明如下结构是非常常见和有用的:
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;
};
来源:http://allenchou.net/2013/12/game-physics-motion-dynamics-implementations/
这里有许多属性可以直接从其他属性计算出来,例如 m_inverseMass
来自 m_mass
。在像 Haskell 这样的无状态编程语言中,获取派生值非常容易:
data RigidBody = RigidBody {mass :: Float}
inverseMass :: RigidBody -> Float
inverseMass body = 1 / mass body
但这会在我们每次需要时计算 inverseMass
,这可能会变得昂贵,尤其是在性能至关重要的领域,例如物理模拟。我考虑过记忆,但我不确定这是否是表达依赖属性惰性评估的好方法,因为它似乎是一个复杂的解决方案。我如何存储派生值而不必重新计算它们?
您可以在 RigidBody
中将 inverseMass
存储为 Maybe Float
。当 inverseMass
为 Just someMass
时,您只需提取此值。如果它是 Nothing
,则计算它并存储在 RigidBody
中。问题出在这个 store 部分。因为您可能知道 Haskell.
中的对象是不可变的
天真但简单的解决方案是 return 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
类型的对象来存储一些状态。因为状态是隐式的,所以您不需要在函数中指定它们更改 RigidBody
的字段。在 Haskell(以及所有优秀的编程语言)中,您可以明确指定您的状态是什么以及您将如何更改它。您明确指定要使用的对象。 inverseMass
monadic 动作(或者如果你想要的话只是函数)将根据调用此函数时的当前状态更新你的显式状态。对于此类任务,这或多或少是 Haskell 中的惯用方法。
好吧,另一个惯用的解决方案:只需创建数据类型的值,并将所有字段设置为某些函数调用。因为 Haskell 是惰性的,所以只有在需要时才会第一次计算这些字段。
正如@4castle 和@Shersh 指出的那样,一种简单的方法是将派生值包含在数据类型中:
data RigidBody = RigidBody
{ m_mass :: Float
, m_inverseMass :: Float }
然后使用智能构造函数创建新的RigidBody
s:
rigidBody mass = RigidBody mass (1/mass)
表达式 1/mass
将为 m_inverseMass
创建一个 thunk,它在第一次求值后无需重新计算即可使用,因此它提供了一种自动记忆。
更一般的转换,例如更改位置和根据 local*
值正确更新所有 global*
字段,将以类似方式处理。作为一个简化的例子:
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。
在imperative/object面向可变状态的编程中,声明如下结构是非常常见和有用的:
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;
};
来源:http://allenchou.net/2013/12/game-physics-motion-dynamics-implementations/
这里有许多属性可以直接从其他属性计算出来,例如 m_inverseMass
来自 m_mass
。在像 Haskell 这样的无状态编程语言中,获取派生值非常容易:
data RigidBody = RigidBody {mass :: Float}
inverseMass :: RigidBody -> Float
inverseMass body = 1 / mass body
但这会在我们每次需要时计算 inverseMass
,这可能会变得昂贵,尤其是在性能至关重要的领域,例如物理模拟。我考虑过记忆,但我不确定这是否是表达依赖属性惰性评估的好方法,因为它似乎是一个复杂的解决方案。我如何存储派生值而不必重新计算它们?
您可以在 RigidBody
中将 inverseMass
存储为 Maybe Float
。当 inverseMass
为 Just someMass
时,您只需提取此值。如果它是 Nothing
,则计算它并存储在 RigidBody
中。问题出在这个 store 部分。因为您可能知道 Haskell.
天真但简单的解决方案是 return 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
类型的对象来存储一些状态。因为状态是隐式的,所以您不需要在函数中指定它们更改 RigidBody
的字段。在 Haskell(以及所有优秀的编程语言)中,您可以明确指定您的状态是什么以及您将如何更改它。您明确指定要使用的对象。 inverseMass
monadic 动作(或者如果你想要的话只是函数)将根据调用此函数时的当前状态更新你的显式状态。对于此类任务,这或多或少是 Haskell 中的惯用方法。
好吧,另一个惯用的解决方案:只需创建数据类型的值,并将所有字段设置为某些函数调用。因为 Haskell 是惰性的,所以只有在需要时才会第一次计算这些字段。
正如@4castle 和@Shersh 指出的那样,一种简单的方法是将派生值包含在数据类型中:
data RigidBody = RigidBody
{ m_mass :: Float
, m_inverseMass :: Float }
然后使用智能构造函数创建新的RigidBody
s:
rigidBody mass = RigidBody mass (1/mass)
表达式 1/mass
将为 m_inverseMass
创建一个 thunk,它在第一次求值后无需重新计算即可使用,因此它提供了一种自动记忆。
更一般的转换,例如更改位置和根据 local*
值正确更新所有 global*
字段,将以类似方式处理。作为一个简化的例子:
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。