如何在 Haskell 中对继承进行建模?



我正在尝试创建一个由几种不同类型组成的游戏引擎:

data Camera = Camera ...
data Light = SpotLight ... | DirectionalLight ...
data Object = Monster ... | Player ... | NPC ...

但是,我现在正在尝试为所有这些实体实现基本物理。这要求它们各自包含一个pos :: (Double, Double, Double)和一个velocity :: (Double, Double, Double)

在面向对象的语言中,我会这样实现它:

Camera implements PhysicalObject

其中PhysicalObject包含两个属性posvelocity

我的第一反应是将它们全部放在同一类型中:

data Object = Monster ... | Player ... | NPC ... | Camera ...

但是,我担心这可能会使实现相机特定功能、光线特定功能等变得困难。实际上,除了它们在世界上都拥有物理位置和速度之外,它们几乎没有其他共同点。

有没有比在每个类型构造函数中定义两个属性更简单的方法?

我可以想到两种方法 - 类型类和镜头。

类型类

class PhysicalObject m where
position :: m -> (Double, Double, Double)
velocity :: m -> (Double, Double, Double)

然后,您将按照以下行为对象创建实例

data Camera = Camera 
{ cameraPosition :: (Double,Double,Double)
, cameraVelocity :: (Double,Double,Double)
}
instance PhysicalObject Camera where
position       = cameraPosition
cameraVelocity = cameraVelocity

对于您的其他类型的类型也是如此。那么任何不需要知道对象细节的函数都可以要求它的参数是PhysicalObject的实例,例如:

type TimeInterval = Double
newPosition :: PhysicalObject m => TimeInterval -> m -> (Double,Double,Double)
newPosition dt obj = (x + du * dt, y + dv * dt, z + dw * dt)
where
(x,y,z) = position obj
(u,v,w) = velocity obj

但是,您将很难编写使用此代码修改对象的函数 - 该类告诉Haskell它如何访问对象的位置和速度,但不知道如何修改它们。

镜头

另一种选择是转到镜头库。这有点像野兽,但它允许你编写一些非常自然的代码。首先,有一些样板

{-# LANGUAGE TemplateHaskell #-}
import Control.Lens

现在定义一些位置和速度数据类型。不要担心以下划线为前缀的奇怪字段名称 - 我们不会使用它们。

data Pos = Pos { _posX, _posY, _posZ :: Double }
data Vel = Vel { _velX, _velY, _velZ :: Double }
instance Show Pos where show (Pos x y z) = show (x,y,z)
instance Show Vel where show (Vel x y z) = show (x,y,z)

现在,您可以使用一些模板Haskell来为您的数据类型派生镜头。这将生成类型类HasPosHasVel,其方法允许您访问和修改作为这些类实例的任何值。

makeClassy ''Pos
makeClassy ''Vel

现在定义相机类,其中包括位置和速度。

data Camera = Camera
{ _cameraPos :: Pos
, _cameraVel :: Vel } deriving (Show)

Template Haskell的另一部分将自动创建cameraPoscameraVel的功能,使您可以访问和修改相机的位置和速度。

makeLenses ''Camera

最后,声明您的相机是HasPos类和HasVel类的实例,并具有其方法的默认实现。

instance HasPos Camera where pos = cameraPos
instance HasVel Camera where vel = cameraVel

现在我们已经准备好做一些真正的工作了。让我们定义一个示例相机

camera = Camera (Pos 0 0 0) (Vel 10 5 0)

修改相机的函数,返回具有更新位置的新相机,是

move :: (HasPos a, HasVel a) => TimeInterval -> a -> a
move dt obj = obj
& posX +~ dt * obj^.velX
& posY +~ dt * obj^.velY
& posZ +~ dt * obj^.velZ

请注意,这是一个完全通用的函数,用于移动任何具有位置和速度的对象 - 它根本不特定于Camera类型。它还具有看起来很像命令式代码的优点!

如果您现在将所有这些加载到 GHCI 中,您可以看到它的实际效果

>> camera
Camera {_cameraPos = (0.0,0.0,0.0), _cameraVel = (10.0,5.0,0.0)}
>> move 0.1 camera
Camera {_cameraPos = (1.0,0.5,0.0), _cameraVel = (10.0,5.0,0.0)}

我会实现它类似于:

type Position = (Double, Double, Double)
type Velocity = (Double, Double, Double)
class PhysicalObject a where
pos :: a -> Position
velocity :: a -> Velocity
data Camera = Camera
{ camPos :: Position
, camVel :: Velocity
} deriving (Eq, Show)
instance PhysicalObject Camera where
pos = camPos
velocity = camVel

然后,您可以对定义的需要PhysicalObject的每个类型执行类似的操作。

您需要开始依赖类型类和对象编码之类的东西。第一种方法是将公共接口编码为每个类型继承自的类型类。

class PhysicalObject o where
pos      :: o -> Vector3
velocity :: o -> Vector3

二是构建一个共同的对象

data PhysicalObject = PhysicalObject { poPos :: Vector3, poVelocity :: Vector3 }
data Monster = Monster { monsterPO :: PhysicalObject
, ... monsterStuff ...
}

甚至可以用于实例化第一个类型类

instance PhysicalObject PhysicalObject where
pos      = poPos
velocity = poVelocity
instance PhysicalObject Monster where
pos      = pos      . monsterPO
velocity = velocity . monsterPO

但是,要小心这样的类型类编码,因为过多地使用它们通常会在阅读代码时造成歧义。可能很难理解类型并知道正在使用哪个实例。

最新更新