在Haskell中编程时如何纠正我的OOP倾向

med*_*ans 13 oop haskell functional-programming

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

我知道我应该抽象出功能并将它们放在一个列表中,但我常常在编写代码的过程中感到被阻止.在这些情况下我应该有什么样的心态?

J. *_*son 15

你正在寻找一些优秀的代码.让我再向一个类似Haskell的解决方案推进几步.

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

data Piece = PipePiece   Pipe
           | TankPiece   Tank
           | BowlPiece   Bowl
           | CrossPiece  Cross
           | SourcePiece Source
Run Code Online (Sandbox Code Playgroud)

这会让你写一些像这样的作品

type Kit = [Piece]
Run Code Online (Sandbox Code Playgroud)

但要求当你消费你的时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
Run Code Online (Sandbox Code Playgroud)

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

这消除了许多冗余类型构造函数,代价是不再能够反映函数类型中的哪种类型 - 我们不再能够编写

rotateBowl :: Bowl -> Bowl
rotateBowl (Bowl orientation) = Bowl (rotate orientation)
Run Code Online (Sandbox Code Playgroud)

但反而

rotateBowl :: Piece -> Piece
rotateBowl (Bowl orientation) = Bowl (rotate orientation)
rotateBowl somethingElse      = somethingElse
Run Code Online (Sandbox Code Playgroud)

这很烦人.

希望这突出了这两个模型之间的一些权衡.至少有一个"更具异国情调"的解决方案,它使用类型类并ExistentialQuantification"忘记"除界面之外的所有内容.这是值得探索的,因为它非常诱人,但被认为是一个Haskell反模式.我先描述一下然后谈谈更好的解决方案.

要使用ExistentialQuantification我们删除sum类型Piece并为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
Run Code Online (Sandbox Code Playgroud)

这是一个反模式,因为ExistentialQuantification会导致更复杂的类型错误并删除大量有趣的信息.通常的理由是,如果你要删除除了能力的所有信息meltPiece,你应该刚刚融化它开始.

myScrapMetal :: [ScrapMetal]
myScrapMetal = [melt Cross, melt Source Vertical]
Run Code Online (Sandbox Code Playgroud)

如果您的类型类具有多个函数,那么您的实际功能可能存储在该类中.举例来说,假设我们可以melt一个piece,也sell它,也许是更好的抽象会是以下

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

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

myKit :: [Piece]
myKit = [pipe UpLeft, pipe UpRight]
Run Code Online (Sandbox Code Playgroud)

老实说,这几乎就是你通过这种ExistentialQuantification方法获得的,但更直接.当你删除类型信息时,forgetPiece只留下类型类词典class Piece- 这正是类型类中函数的一个产物,这是我们用data Piece刚才描述的类型明确建模的.


我可以想到使用的一个原因ExistentialQuantification最好的例子是Haskell的Exception系统 - 如果你有兴趣,看看它是如何实现的.缺点是Exception必须设计成任何人都可以Exception在任何代码中添加新内容并使其可以通过共享 Control.Exception机器路由,同时保持足够的身份以便用户捕获它.这也需要Typeable机械......但它几乎肯定是矫枉过正的.


需要注意的是,您使用的模型在很大程度上取决于您最终消耗数据类型的方式.初始编码,你把所有东西都表示为一个抽象的ADT,就像data Piece解决方案一样好,因为它们丢弃了很少的信息......但也可能既笨拙又慢.像melt/ selldictionary 这样的最终编码通常更有效,但需要更深入地了解Piece"手段"及其使用方式.


Sas*_* NF 0

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

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

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