gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-12-02 18:00:11 25 4
gpt4 key购买 nike

在 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]

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

最佳答案

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

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

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

它可以让你写一个像这样的列表

type Kit = [Piece]

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

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

这消除了许多冗余的类型构造函数,但代价是不再能够反射(reflect)函数类型中的片段类型 - 我们不再可以编写

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

但是相反

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

这很烦人。

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

要使用ExistentialQuantification,我们删除总和类型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 会导致更复杂的类型错误并删除大量有趣的信息。通常的论点是,如果你要删除除了熔化碎片之外的所有信息,你应该一开始就熔化它。

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

如果您的类型类具有多个函数,那么您真正的功能可能存储在该类中。例如,假设我们可以熔化一 block 碎片出售它,也许更好的抽象如下

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的类型类字典——这正是类型类中函数的产物,这就是我们的使用刚刚描述的数据 block 类型重新显式建模。

<小时/>

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

<小时/>

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

关于oop - 在 Haskell 中编程时如何纠正我的 OOP 倾向,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/18867620/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com