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

use*_*220 15 haskell

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

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

现在我已经读过一些人认为即使使用C++,类层次结构也是错误的架构.但至少它会尝试代码重用.并且显而易见的是能够将所有东西都推到一个管理容器中,因为所有东西都很容易归入CEntity列表.

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

not*_*job 16

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

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

(你并不需要的一切推到一个容器中,除非你错过了世界管理更新的高级方式,并被迫沿着一些收集应用迭代.update()方法.官能团反应性编程是管理更新的高级方式.)

学习思考玻璃钢需要时间,但投资是值得的.

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

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

这些往往比亚型多态性应用得更广泛.

  • 顺便说一下,在我看来,"Functor","Applicative","Foldable","Traversable"和"Monad"都将在OO中被称为模式,你必须手写样板代码来实现模式.在哈斯克尔,他们就在那里.我称之为:'Functor` =数据转换模式,`Applicative` =组合模式,`Foldable` =组合访客模式,`Traversable` =结构访问者模式,`Monad` =重新编程分号模式.我不使用访问者模式来查找我的树值的总和或最大值,我只是创建一个实例和`fol​​dr(+)`或`foldr max`. (8认同)
  • "重新编程分号模式" - 我喜欢它:) (2认同)

Pau*_*aul 8

我先说我对游戏开发一无所知,所以我的回答可能不适用于你的问题.

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

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

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

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

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

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

编辑(回应Zeta的评论):

确实,类型类不允许在列表等数据类型中使用异构类型,但是如果有额外的辅助数据类型,这也可以实现(从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
Run Code Online (Sandbox Code Playgroud)

  • 值得注意的是,虽然这个ShowBox示例确实演示了存在的异构列表,但它并不是一个使用它的好地方.你想用一个更强大的类型类(或一组类型类)约束存在性,这样当你把它从列表中拉回来时,仍然有一些有趣的操作要做.否则,你可能刚刚开始在整个事情上运行`map show`. (4认同)
  • "我先说我对游戏开发一无所知"应该不是问题.游戏开发并不神奇,让好的想法突然变得有意义. (2认同)

hug*_*omg 5

Haskell没有子类型,因此很难直接转换层次结构.你可以试着用类型类做一些疯狂的黑客但我不推荐它,因为它很快变得非常复杂.

您链接到的基于组件的体系结构与代码重用一样好,并且更容易转换为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);
};
Run Code Online (Sandbox Code Playgroud)

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

并且具体类的构造函数只是返回其中一个记录的函数.

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

medkitRenderer :: Color -> Renderer
medkitRenderer rgb = -- ...
Run Code Online (Sandbox Code Playgroud)

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

renderers :: [Renderer]
renderers = [ humanRenderer 10, humanRenderer 20, medKitRenderer Red ]
Run Code Online (Sandbox Code Playgroud)