切换到Haskell时如何避免OOP深层次结构?

时间:2014-02-11 09:27:53

标签: haskell

用C ++编写的游戏通常具有类层次结构,例如

  • CEntity
    • CMoveable
      • CCAR
      • CTank
      • CJetPack
    • CHuman
      • CPedestrian
      • CPlayer
      • CAlien
    • CRigid
      • 瓦罐
      • CGrenade
    • CMissile
    • CGun
    • CMedkit

现在我已经读过一些人argued,即使使用C ++,类层次结构也是错误的架构。但至少它会尝试代码重用。这是显而易见的方式,可以将所有东西都推到一个管理容器中,因为所有东西都可以轻松地放入CEntity列表中。

但无论如何,对于那些试图从C ++切换到Haskell来制作游戏的人来说,如何改变体系结构以适应Haskell的功能范式呢?

5 个答案:

答案 0 :(得分:16)

我认为将OO代码转换为Haskell是错误的,而是在Haskell中从头开始编写游戏。

在我看来,最适合用于游戏编程的工具是功能反应式编程。这使您在行为和事件方面进行思考 - 您的游戏元素会随着时间的推移而发生变化,您可以将它们组合起来并定义它们之间的关系。

(除非您缺少管理世界更新的高级方法,并且不得不使用.update()方法迭代某些集合,否则您不需要将所有内容都推送到单个容器中。功能性反应式编程是一种管理更新的高级方法。)

学习思考FRP需要时间,但投资是值得的。

代码重用(通常在Haskell中)通过

  • 非常通用的类型签名 - 基于多态或类型类
  • 高阶函数
  • 很棒的抽象,例如FunctorApplicativeFoldableTraversableMonad
  • 避免不必要地制作不纯净的代码 - 纯代码最容易组合并重新应用
  • 发现事物的行为或结合方式的相似之处 - 不要写两次相同的代码
  • 报废你的锅炉
  • 模板Haskell

这些往往比子类型多态性应用得更广泛。

答案 1 :(得分:8)

首先我要说的是我对游戏开发一无所知,所以我的回答可能不适用于您的问题。

话虽这么说,我认为关键是要问一个问题:为什么你在C ++这样的语言中使用类层次?我认为答案是双重的:子类型多态性和代码重用。

正如您所指出的,使用继承来实现代码重用通常会受到批评,我认为是正确的。首选组合继承通常是很好的建议,它减少了耦合并使事情更加明确。 Haskell中的等价物只是重用函数,这非常简单。

这给我们带来了第二个好处:亚型多态性。 Haskell不支持子类型或子类型polymorphsim,但是对于类型类,它有另一种特殊的多态性,甚至更普遍(因为事物不需要处于子类型关系中来实现相同的函数)。 / p>

所以我的答案是:想想为什么你想要一个班级层次。如果你想要代码重用,那么只需要通过将代码分解成足够通用的函数来重用代码并重用这些代码,如果你想要多态就可以使用类型类。

在某些情况下,子类型实际上是有用的,因此有时Haskell不支持这一点的缺点,但根据我的经验,这是非常罕见的。另一方面,继承往往在C ++或Java等语言中被过度使用,因为这是他们提供的一刀切的工具。

总的来说,我同意@ enoughreptocomment的回答,即在Haskell中重现OO设计是错误的 - 你通常可以做得更好!我只是想指出类层次结构给你的东西以及在Haskell中可以实现的类似事情。

编辑(响应Zeta的评论):

确实,类型类不允许在列表等数据类型中使用异构类型,但是如果有额外的辅助数据类型,这也可以实现(stolen from the Haskell wikibook):

{-# LANGUAGE ExistentialQuantification #-}

data ShowBox = forall s. Show s => SB s

heteroList :: [ShowBox]
heteroList = [SB (), SB 5, SB True]

instance Show ShowBox where
  show (SB s) = show s

f :: [ShowBox] -> IO ()
f xs = mapM_ print xs

main = f heteroList

答案 2 :(得分:5)

Haskell没有子类型,因此很难直接转换层次结构。您可以尝试使用类型组来做一些疯狂的黑客攻击I wouldn't recommend it,因为它很快变得非常复杂。

您链接到的基于组件的体系结构与代码重用一样好,并且更容易转换为Haskell,因为没有类层次结构。

例如,在C ++中,您将拥有一个渲染组件。在C ++中,您可以将其表示为抽象渲染界面和一些具体的Render类。

class Renderer {
    virtual void draw(double x, double y) = 0;
    virtual void frobnicate(int n) = 0;
};

class HumanRenderer: public Renderer {
  //render Players and Pedestrians...
  //(code reuse!)

  //constructor:
  HumanRenderer(int age);
};

class MedkitRenderer: public Renderer{
  //render the medkit

  //constructor:
  HumanRenderer(Color color);
};

在Haskell中,如果没有子类型,你会做类似的事情。父接口的类型只是函数的记录:

data Renderer = Renderer {
  rendererDraw :: Double -> Double -> IO (),
  rendererFrobnicate :: Int -> IO ()
}

-- I'm putting everything in the IO monad so the code is side effecting like
--in the C++ version. If you want to avoid this mutation then this is where that 
--functional reactive programming stuff would come in.

,具体类的构造函数只是返回其中一条记录的函数。

humanRenderer :: Int -> Renderer
humanRenderer age = -- ...

medkitRenderer :: Color -> Renderer
medkitRenderer rgb = -- ...

请注意,由于存在单个“渲染器”类型,因此您可以将不同类型的渲染器放在同源列表中,就像在cpp中一样(这在类型类方法中会更加棘手):

renderers :: [Renderer]
renderers = [ humanRenderer 10, humanRenderer 20, medKitRenderer Red ]

答案 3 :(得分:2)

该问题要求具有两个目标的类层次结构的Haskell编码:

  1. 能够将所有东西都推到一个管理容器中
  2. 代码重用
  3. 我将从我的示例问题中使用类层次结构的较小变体。实现目标1的最简单方法是为实体提供单个代数数据类型。然后我们可以使用列表或数组或我们想要包含实体的任何容器。所以我们想:

    data Entity = ...
    type ExampleContainer = [Entity]
    

    我们应该如何填写...?我首先展示了一种天真的方法,分析了为什么它无法提供重用,然后将这种洞察力转化为一种提供重用的更复杂的方法。

    天真的方法,糟糕的重复使用:(

    CEntity类层次结构中有多种实体,因此我们可以为实体数据类型使用多个构造函数:

    data Entity
      = Car     Position Velocity Color
      | Player  Position Velocity Gun
      | Door    Position Key
      | Rock    Position
    

    类层次结构的每个叶子对应一个构造函数,但中间类不会显示。这导致数据类型声明重复:我们多次重复PositionVelocity。类型级别的这种重复也会影响我们程序的其余部分:例如,以一步向前移动对象的函数看起来像:

    move :: Entity -> Entity
    move (Car    position velocity color) = Car  (position + velocity) velocity color
    move (Player position velocity gun)   = Tank (position + velocity) velocity gun
    move (Door   poosition key)           = Door position key
    move (Rock   position)                = Rock position
    

    PositionVelocity字段的重复确实会导致position + velocity公式的重复。也许如果我们重复使用代数数据类型中的PositionVelocity字段,我们也可以重用position + velocity公式?

    复杂的方法,更好的重用:)

    我们重构代数数据,以便共享公共字段。所有实体都有一个位置,但其他字段根据我们的实体类型而不同:

    data Entity
      = Entity Position EntityInfo
    

    移动物体具有速度但固定物体不具有:

    data EntityInfo
      = Moving Velocity Moving
      | Fixed Fixed
    

    移动物体可以是汽车或玩家:

    data Moving
      = Car Color
      | Player Name
    

    固定物体可以是门或石头:

    data Fixed
      = Door Key
      | Rock
    

    所以我们仍然有四个构造函数CarPlayerDoorRock,但另外我们有构造函数Entity,{{1和Moving存储可用于多种实体的信息。这些附加构造函数对应于类层次结构中的中间类。请注意,我们只提及FixedPosition一次,因此希望Velocity函数中的代码重复应该消失。事实上:

    move

    现在,公式move :: Entity -> Entity move (Entity position (Moving velocity info)) = Entity (position + velocity) (Moving velocity info) move (Entity position (Fixed info)) = Entity position (Fixed info) 只出现一次,正如我们所希望的那样。

    摘要

    编码深层次结构的一种方法是使用代数数据类型。每个类对应一个构造函数,每个具有子类的类也对应一个数据类型。如果我们避免在这些数据类型中进行字段重复,我们还会避免在操作数据类型值的代码中进行代码重复。

答案 4 :(得分:1)

我认为这是一个相当难以回答的问题,因为它可能高度依赖于个人编码风格。在我在Haskell中编写任何内容之前,我已经从大型继承层次结构切换到不同的设计方式。

根据我的观察,Haskell中的代码重用比C ++或Java中的更简单,因为您使用的几乎所有内容,每个原语和元素都以可预测的,类似的方式运行,并且可以使用一小组非常通用的函数进行操作。这意味着只要您在创建实体时遵守管理Haskell惯用结构的规则,代码不会重复的事实应该是自然而然的。

举个例子,看看fmap。它是一个非常简单但功能非常强大的工具。我可以想象你使用你的玩家作为Functor并将物品映射到他身上以使它们产生效果。你只写实际效果并定义播放器;你不必担心他们将如何互动,因为有"标准"这样做的方法。

TL; DR 高阶函数和类型类在处理更复杂的逻辑时证明自己非常好。 镜头简化了嵌套数据的操作。你必须重新考虑一些事情,可能你不会找到直接的同行,但在Haskell中编写好的游戏代码当然是可能的。