如何在Haskell中建模继承?

sda*_*das 9 haskell interface

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

data Camera = Camera ...
data Light = SpotLight ... | DirectionalLight ...
data Object = Monster ... | Player ... | NPC ...
Run Code Online (Sandbox Code Playgroud)

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

在面向对象的语言中,我将它实现为:

Camera implements PhysicalObject
Run Code Online (Sandbox Code Playgroud)

其中PhysicalObject包含两个属性posvelocity.

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

data Object = Monster ... | Player ... | NPC ... | Camera ...
Run Code Online (Sandbox Code Playgroud)

然而,我担心这可能会使得实现特定于摄像机的功能,特定于灯光的功能等变得困难.实际上,除了它们在世界上都具有物理位置和速度这一事实之外,它们几乎没有其他共同点. .

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

Chr*_*lor 15

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

输入类

class PhysicalObject m where
  position :: m -> (Double, Double, Double)
  velocity :: m -> (Double, Double, Double)
Run Code Online (Sandbox Code Playgroud)

然后,您将沿着以下行为对象创建实例

data Camera = Camera 
  { cameraPosition :: (Double,Double,Double)
  , cameraVelocity :: (Double,Double,Double)
  }

instance PhysicalObject Camera where
  position       = cameraPosition
  cameraVelocity = cameraVelocity
Run Code Online (Sandbox Code Playgroud)

和你的其他类型相似.然后,任何不需要知道对象细节的函数都可以只需要它的参数是实例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
Run Code Online (Sandbox Code Playgroud)

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

镜头

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

{-# LANGUAGE TemplateHaskell #-}
import Control.Lens
Run Code Online (Sandbox Code Playgroud)

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

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

现在,您使用一些Template Haskell来为您的数据类型派生镜头.这将生成类型类HasPos,HasVel其方法允许您访问和修改作为这些类的实例的任何值.

makeClassy ''Pos
makeClassy ''Vel
Run Code Online (Sandbox Code Playgroud)

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

data Camera = Camera
  { _cameraPos :: Pos
  , _cameraVel :: Vel } deriving (Show)
Run Code Online (Sandbox Code Playgroud)

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

makeLenses ''Camera
Run Code Online (Sandbox Code Playgroud)

最后,声明您的相机是HasPosHasVel类的实例,并使用其方法的默认实现.

instance HasPos Camera where pos = cameraPos
instance HasVel Camera where vel = cameraVel
Run Code Online (Sandbox Code Playgroud)

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

camera = Camera (Pos 0 0 0) (Vel 10 5 0)
Run Code Online (Sandbox Code Playgroud)

修改相机,返回具有更新位置的新相机的功能是

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

请注意,这是一个完全通用的函数,用于移动任何具有位置和速度的对象 - 它根本不是特定于该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)}
Run Code Online (Sandbox Code Playgroud)


bhe*_*ilr 5

我会实现它类似于:

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

然后,您可以针对您定义的每种类型执行类似操作PhysicalObject.