13

在 Haskell 中编程时,我经常遇到这个问题。在某些时候,我尝试模拟一种 OOP 方法。在这里,我正在为我发现的一个 Flash 游戏编写某种 AI,我想将各种片段和关卡描述为片段列表。

module Main where

type Dimension = (Int, Int)
type Position = (Int, Int)
data Orientation = OrienLeft | OrienRight

data Pipe = Vertical | Horizontal | UpLeft | UpRight | DownLeft | DownRight
data Tank = Tank Dimension Orientation
data Bowl = Bowl Dimension
data Cross = Cross
data Source = Source Dimension

-- desired
-- data Piece = Pipe | Tank | Bowl | Cross | Source

-- So that I can put them in a list, and define
-- data Level = [Piece]

我知道我应该把功能抽象出来,把它们放在一个列表中,但是我在编写代码的过程中经常感到受阻。在这些情况下,我应该有什么样的一般心态?

4

4 回答 4

15

您正在编写一些很棒的代码。让我将它推向类似 Haskell 的解决方案。

您已经成功地将每一个建模Piece为一个独立的实体。这看起来完全没问题,但是您希望能够处理碎片集合。最直接的方法是描述可以是任何所需部分的类型。

data Piece = PipePiece   Pipe
           | TankPiece   Tank
           | BowlPiece   Bowl
           | CrossPiece  Cross
           | SourcePiece Source

这会让你写一个像这样的作品列表

type Kit = [Piece]

但要求当您使用您的时Kit,您在不同类型的Pieces上进行模式匹配

instance Show Piece where
  show (PipePiece   Pipe)   = "Pipe"
  show (TankPiece   Tank)   = "Tank"
  show (BowlPiece   Bowl)   = "Bowl"
  show (CrossPiece  Cross)  = "Cross"
  show (SourcePiece Source) = "Source"

showKit :: Kit -> String 
showKit = concat . map show

还有一个强有力的论据是Piece通过“扁平化”一些冗余信息来降低类型的复杂性

type Dimension   = (Int, Int)
type Position    = (Int, Int)
data Orientation = OrienLeft | OrienRight
data Direction   = Vertical | Horizontal | UpLeft | UpRight | DownLeft | DownRight

data Piece = Pipe Direction
           | Tank Dimension Orientation
           | Bowl Dimension
           | Cross
           | Source Dimension

它消除了许多冗余的类型构造函数,代价是不再能够反映函数类型中的类型——我们不再可以编写

rotateBowl :: Bowl -> Bowl
rotateBowl (Bowl orientation) = Bowl (rotate orientation)

但反而

rotateBowl :: Piece -> Piece
rotateBowl (Bowl orientation) = Bowl (rotate orientation)
rotateBowl somethingElse      = somethingElse

这很烦人。

希望这能突出这两种模型之间的一些权衡。至少有一个“更奇特”的解决方案,它使用类型类并ExistentialQuantification“忘记”除了接口之外的所有内容。这是值得探索的,因为它很诱人,但被认为是 Haskell 反模式。我会先描述它,然后再讨论更好的解决方案。

要使用ExistentialQuantification,我们删除 sum 类型Piece并为碎片创建一个类型类。

{-# LANGUAGE ExistentialQuantification #-}

class Piece p where
  melt :: p -> ScrapMetal

instance Piece Pipe
instance Piece Bowl
instance ...

data SomePiece = forall p . Piece p => SomePiece p

instance Piece SomePiece where
  melt (SomePiece p) = melt p

forgetPiece :: Piece p => p -> SomePiece
forgetPiece = SomePiece

type Kit = [SomePiece]

meltKit :: Kit -> SomePiece
meltKit = combineScraps . map melt

这是一种反模式,因为ExistentialQuantification会导致更复杂的类型错误和大量有趣信息的擦除。通常的论点是,如果您要擦除除 的能力之外的所有信息meltPiece您应该从一开始就将其融化。

myScrapMetal :: [ScrapMetal]
myScrapMetal = [melt Cross, melt Source Vertical]

如果您的类型类具有多个功能,那么您的真正功能可能存储在该类中。例如,假设我们可以meltapiecesell它,也许更好的抽象是以下

data Piece = { melt :: ScrapMetal
             , sell :: Int
             }

pipe :: Direction -> Piece
pipe _ = Piece someScrap 2.50

myKit :: [Piece]
myKit = [pipe UpLeft, pipe UpRight]

老实说,这几乎正是您通过该ExistentialQuantification方法获得的结果,但更直接。当您通过删除类型信息时,forgetPiece只留下类型类字典class Piece--- 这正是类型类中函数的产物,这是我们使用data Piece刚刚描述的类型显式建模的内容。


我能想到使用的一个原因ExistentialQuantification是 Haskell 的系统最好的例证Exception——如果你有兴趣,看看它是如何实现的。简而言之,它的Exception设计必须使任何人都可以Exception在任何代码中添加新代码,并使其可通过共享 Control.Exception机器进行路由,同时保持足够的身份让用户也能捕捉到它。这Typeable也需要机器……但这几乎可以肯定是矫枉过正。


外卖应该是您使用的模型将很大程度上取决于您最终如何使用您的数据类型。将所有内容表示为抽象 ADT(如data Piece解决方案)的初始编码很好,因为它们丢弃的信息很少……但也可能既笨拙又缓慢。melt/字典之类的最终编码sell通常更有效,但需要更深入地了解Piece“含义”以及如何使用它。

于 2013-09-18T21:44:53.347 回答
1

10 年前,当我学习 Haskell 时,我也遇到过类似的问题。让我以比您上面的示例更笼统的方式回答这个问题。


TL;博士:

考虑类似 OOP 的对象,代表值对象和函数对象,大量使用 c++Templates/javaGenerics。您的程序设计应该基于函数的数据流,而不是存储中间对象状态的可变内存引用。这些函数对象在运行时是可组合的。


您的陈述“在某些时候,我尝试模拟 OOP 方法。” 描述您设计程序的经验方法。正如您可能在某处听说过的,函数式编程对于 OOP 程序员来说被认为比新手更难学习,因为首先需要摒弃“错误的概念”。如果您知道如何利用它来发挥自己的优势,那并不完全正确:

诀窍是利用您的 OOP 经验,或者更有可能是您对“对象”的想象,以相反的方向:您将如何设计带有对象的 haskell 函数和 haskell 值?您将如何重组您的程序以仅使用这些新概念来设计合理的数据流?在函数式编程中,每个“值”都是一个只有 final/const 属性的对象——或者是一个允许延迟初始化的 getter(并且它允许实现看似无限的单链表)。“功能”也是这样的“价值”。如果您使用这些概念来考虑您的程序,那么您将很容易掌握这一点,而无需 OOP 设计模式,而是使用实际上是函数和值的“对象”。

如果 OOP 是关于管理存储可变状态/值的内存单元,那么函数就是关于从前一个状态/值计算下一个状态/值。在某些时候,您会看到您只考虑为一个健全的数据流组合功能,而不是管理存储值的内存位置。

下一步是将类型类的实例视为某种全局实例化的字典对象,这些字典对象会自动作为不可见参数传递给函数。这些字典对象的 OOP 类将使用 c++Templates/javaGenerics 作为类型参数,并且它们的方法与它们的参数和/或返回值相关,而不是任何“this”引用。一旦您了解何时以及如何使用类型类,它们将成为您的类型的属性/角色/风格,而不是想象的对象。

恕我直言,类型类是最好/最健全的 OOP 设计模式之一,但它们只为极少数程序员所知,并且如果没有功能性思维就不容易重新发明。(我个人认识的每个有经验的 Haskell 程序员都将类型类重新发明为 c++Templates/objC/js 中的设计模式......)

学习用 haskell 或任何其他新的陌生语言思考的一般方法是提出以下问题:“我可以轻松地用这种语言做什么以及如何做?” 而不是问题:“我怎样才能写出这个真正适合我已经非常有经验的完全不同的语言的特定设计?” (这听起来很明显,但我们往往会忘记这一点。一次又一次。)


如果您的程序确实需要更多 OOP 意义上的对象,那么您可能正在寻找 haskells 记录(如果您是初学者)。如果这些对象代表数据库或类似的东西,您可能正在寻找镜头库;但是对于初学者来说,镜头可能(或可能不是)非常先进/刺激。镜头是可组合的 getterAndSetter “对象”(或值或类似功能的东西),在 OOP 中的使用类似于“obj.lens1.lens2.modify()”——它们在 OOP 中是可堆叠的,但既不是可组合的,也不是对象本身。

于 2013-09-26T17:53:13.027 回答
1

在我看来,你的思维方式没有问题:它相当抽象,这很好。

正如Sassa NF 所建议的,您可以使用类型类,这将非常优雅。但是在您的示例中,我会使用abstract data type以“更简单的方式”对其进行扩展,因为这似乎是您思考的“自然方式”。

从这个意义上说,您的示例将类似于,例如:

data Piece = Vertical 
           | Horizontal 
           | UpLeft 
           | UpRight 
           | DownLeft 
           | DownRight
           | Cross
           | Bowl Dimension
           | Source Dimension
           | Tank Dimension Orientation

并重复一遍:我没有看到你建模问题的方式有问题,因为它对我来说似乎足够抽象。

于 2013-09-18T10:31:47.917 回答
0

您正在考虑多态性。在 Haskell 中也有这样的地方,只是做得不同。

例如,您似乎想以通用方式处理关卡中的 Pieces。那是什么处理?如果你能定义这些函数,你会发现这就像定义一个 Piece 接口。在 Haskell 中,这将是一个类型类(定义为class Piece a“实现”应该处理的函数列表)。

然后,您需要定义这些函数对特定数据类型的作用,例如instance Piece Pipe并添加这些函数的定义。为所有数据类型完成此操作后,您可以将它们添加到 Pieces 列表中。

于 2013-09-18T09:16:43.167 回答