抽象记录或记录界面?

时间:2016-11-02 18:49:11

标签: haskell interface abstract

我使用Haskell制作游戏,(这是一项任务,所以不要评判我),但我面临着有关数据类型的问题。

所以我想要的是一个数据类型实体,它具有位置,速度,角度和旋转速度。一个记录非常适合这个想法:

data Entity = Entity {
    location :: Vector,
    velocity :: Vector,
    angle    :: Float,
    rotation :: Float
}

现在我想要实体的实例,即Player Rock Pickup和Bullet。但是Players Rocks和Bullets必须有一个额外的字段,即health :: Int,而Pickup必须有另外一个字段,即pickupType :: PickupType。

但我有一些方法可以处理任何实体类型。例如:

move :: Entity -> Entity
move e@(Entity {location, velocity, angle, rotation}) = e {location = location + velocity, angle = angle + rotation}

我不知道如何做到这一点,或者甚至可能做到这一点。我不明白为什么如果不可能,因为这在其他语言中肯定是可能的。

有些尝试以及为什么它们不是我想要的:

尝试1:

type Player = Player {
    e      :: Entity,
    health :: Int
}

这很有效,但真的很难看。例如,这就是你如何移动玩家:

movePlayer :: Player -> Player
movePlayer p@(e) = p {e = move e}

这真的太丑了。

肯定: 易于创建抽象类。 易于创建实例。 简单的抽象方法。

否定: 很难获取或设置实例的实体实现字段。

尝试2:

class Entity e where
    getLocation :: e -> Vector
    getVelocity :: e -> Vector
    ...
    setLocation :: Vector -> e -> e
    setVelocity :: Vector -> e -> e
    ...

data Player = Player {
    playerLocation :: Vector,
    playerVelocity :: Vector,
    ...
    playerHealth   :: Int
}

instance Entity Player where
    getLocation = location
    getVelocity = velocity
    ...
    setLocation l e = e {location = l}
    setVelocity v e = e {playerVelocity = v}
    ...

move :: (Entity e) => e -> e
move e = (setLocation (getLocation e + getVelocity e) . setAngle (getAngle e + getRotation e)) e

它确实有效,但我希望我们都同意他们的定义现在真的很难看。适用于任何实体的抽象方法也变得丑陋。唯一的好处是像movePlayer这样的方法变得非常简单。

movePlayer :: Player -> Player
movePlayer = move

我甚至不需要再定义movePlayer,因为我可以使用move。

肯定: 易于获取或设置实例的实体实现字段。

否定: 很难创建抽象类。 更难创建实例。 硬抽象方法。

尝试3:

为实体提供任何实例所需的所有字段。

data Entity = Entity {
    location   :: Vector,
    velocity   :: Vector,
    angle      :: Float,
    rotation   :: Float,
    health     :: Int,
    pickupType :: PickupType
}

这样我甚至不需要定义实例,我只能使用实体。唯一的问题是你有很多多余的数据。这是我目前使用的,IMO是我的问题的最佳解决方案,但我仍然不喜欢它。

肯定: 很容易创建抽象类,即使它不再是抽象的。 无需定义实例。 简单的抽象方法。 易于获取或设置实例的实体实现字段。

否定: 很多未使用的数据。 每次创建实体时都必须定义许多无意义的字段。

所以请帮助我,我找不到比这三个更好的方法:(

4 个答案:

答案 0 :(得分:2)

我会第一次尝试,原因很简单:

它准确地捕捉了Player的意图 - 它是Entity的附加信息。

data Player = Player {
    e      :: Entity
    health :: Int
}

虽然所有处理的函数最初都可能很麻烦,但您可能永远不必再次看到它们,这意味着您在代码中提供了一个足够抽象的接口,而不是直接访问Player的状态。

movePlayer :: Player -> Player
movePlayer p@(e) = p {e = move e}

这个函数写一次,然后理想情况下你再也不用处理内部。

此外,现在您可以按预期使用类型类:您可以在单独的类型类中抽象movePlayer,例如Movable

class Movable m where
    move :: m -> m

-- Obviously, you can move entities
instance Movable Entity where
    move e = -- stuff

但现在移动Players非常容易:

instance Movable Player where
    move (Player entity health) = Player (move entity) health
    -- works, since `Entity` is movable

除此之外,你的类型类方法有一个重大缺陷:Player的函数怎么样但Entity没有呢?在这种情况下,您将Player继承Entity,如下所示:

class Entity e => Player e where
   -- stuff ...

但是由于Haskell的类型类是开放的,任何东西都可以成为Player,这不是它应该如何工作(当然,除非你的意图)。 / p>

答案 1 :(得分:2)

我会说你的第一次尝试是要走的路,原因与@ ThreeFx的回答相同。我会提出一个略有不同的选择。

鉴于以下类型:

data Player = Player {
    playerEntity :: Entity,
    health       :: Int
}

data Pickup = Pickup {
    pickupEntity :: Entity,
    pickupType   :: PickupType
}

我们可以提供通用的高阶函数,以便更轻松地对Entity执行Entity操作,而不是为Player上的每个操作设置单独的类型类。 } s和Pickup s:

overPlayerEntity :: (Entity -> Entity) -> Player -> Player
overPlayerEntity fn (Player pe h) = Player (fn pe) h

overPickupEntity :: (Entity -> Entity) -> Pickup -> Pickup
overPickupEntity fn (Pickup pe t) = Pickup (fn pe) t

现在,我们可以

movePlayer = overPlayerEntity move
movePickup = overPickupEntity move

我们也可以将它包装成一个类型类,以便更容易编写通用代码:

class HasEntity a where
  overEntity :: (Entity -> Entity) -> a -> a

instance HasEntity Player where overEntity = overPlayerEntity
instance HasEntity Pickup where overEntity = overPickupEntity

这允许这样的事情:

move' :: HasEntity a => a -> a
move' = overEntity move

适用于PlayerPickup。这样就无需使用move等专用版本的函数,同时我们只需要编写一次Entity访问样板。

顺便说一句,这种over...Entity做事方式正在逼近"镜头"在@ duplode和@Paul Johnson的答案结尾处提到的技巧。这些基本上是两个(非常)专用镜头。如果我们添加HasEntity类型类,它会为我们提供所谓的"经典镜头" (这是lens库中使用的术语)。您并不需要担心一般镜头概念的含义或含义,但这可以为您提供一个了解未来镜头的切入点。

答案 2 :(得分:0)

你可以使用一个总和(又名" union")类型。

data Entity =
   Player {
      location :: Vector,
      -- etc.
      health :: Int }
   | Pickup {
      location :: Vector,
      -- etc.
      pickupType :: PickupType}

只需让sum类型保存变化的数据,就可以将其解决。

这样做的好处是你可以拥有一个[实体],当实体的所有不同变体都是不同的类型时(不像OO语言),这是你无法做到的。

根据您的游戏模型,您可能还希望将位置和速度数据与其余玩家信息分开。这些数据能否更好地保存在像四叉树这样的空间数据结构中?这样,您可以将常量数据与每帧中变化的内容分开。

你应该关注的一件事是lenses,它可以解决你用大量的getter和setter描述的问题。

答案 3 :(得分:0)

这是一个冗长的评论,而不是一个答案,因为ThreeFx's answer已经涵盖了我要写的内容。

首先,避免使用OOP术语,因为它会导致混淆。在您的尝试#1中,Entity是一种数据类型,而不是一个类,它也不是抽象的。在尝试#1中PlayerEntity之间的关系只是一个组合:Player有一个Entity,它不是Entity的实例1}}。

其次,记录更新语法 在Haskell中非常难看。这并不会让它变得复杂,只是有点麻烦。所以,当你说,例如“很难获得或设置实例的实体实现的字段”,它实际上并不难,只是不太漂亮。确定数据类型的设计并不是一个严重的问题。

第三,镜头是(在许多其他事物中)避免记录更新语法的丑陋的一种方式。你现在可能不想深入研究(至少等到你完成作业之后),但是我无法抗拒留下一个高度相关的教程链接,供你在将来的某些时候阅读:{ {3}}