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。当 inverseMassJust 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 }

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

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。