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

Aus*_*ett 4 haskell functional-programming lazy-evaluation

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

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;
};
Run Code Online (Sandbox Code Playgroud)

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

这里有许多属性可以直接从其他属性计算,例如m_inverseMassfrom m_mass.在像Haskell这样的无状态编程语言中,获取派生值很容易:

data RigidBody = RigidBody {mass :: Float}

inverseMass :: RigidBody -> Float
inverseMass body = 1 / mass body
Run Code Online (Sandbox Code Playgroud)

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

K. *_*uhr 11

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

data RigidBody = RigidBody
  { m_mass :: Float
  , m_inverseMass :: Float }
Run Code Online (Sandbox Code Playgroud)

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

rigidBody mass = RigidBody mass (1/mass)
Run Code Online (Sandbox Code Playgroud)

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

更一般的转换,例如改变位置和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)
Run Code Online (Sandbox Code Playgroud)

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