Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Haskell evaluating properties of a data type only once when first accessed?

In imperative/object oriented programming with mutable state, it would be very common and useful to declare a structure such as the following:

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;
};

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

There are many properties here that are able to be computed directly from others, such as m_inverseMass from m_mass. In a stateless programming language like Haskell, getting derived values is easy enough:

data RigidBody = RigidBody {mass :: Float}

inverseMass :: RigidBody -> Float
inverseMass body = 1 / mass body

But this computes the inverseMass every time we need it, which can get expensive especially in domains where performance is critical, like physics simulation. I've considered memoization, but I wasn't sure if this is a good way of expressing this lazy evaluation of dependent properties, as it seemed to be a complicated solution. How would I store derivative values without having to recompute them?

like image 433
Austin Garrett Avatar asked Dec 19 '22 03:12

Austin Garrett


1 Answers

As @4castle and @Shersh note, a simple approach would be to include the derived value in the data type:

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

and then use a smart constructor to make new RigidBodys:

rigidBody mass = RigidBody mass (1/mass)

The expression 1/mass will create a thunk for m_inverseMass which, after it is first evaluated, will be available without recalculation, so it provides a sort of auto memoization.

More general transformations, like changing the position and properly updating all the global* fields based on the local* values would be handled in a similar manner. As a simplified example:

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)

In an application that's performance critical, you would want to take steps to introduce strictness in appropriate places so you don't build up huge piles of unevaluated thunks.

like image 112
K. A. Buhr Avatar answered Apr 16 '23 18:04

K. A. Buhr