Haskell中的函数式面向对象编程

3

在阅读"Haskell编程入门"这本书时,我遇到了一个相当令人困惑的练习。其中一个章节讲解如何使用闭包创建简单对象。例如,我们有一个元组来描述原始机器人:(名字,攻击力,血量)。通过使用这个元组,我们可以像这样构造一个机器人:

robot (name,attack,hp)  = \message -> message (name,attack,hp)

例如:
killerRobot = robot ("Kill3r", 25, 200)

接着,作者解释了如何利用这个结构体创建访问器函数:

hp (_,_,hp) = hp
attack (_,a,_) = a

getHP aRobot = aRobot hp
getAttack aRobot = aRobot attack

这样我们就可以检查某个机器人有多少命中点:

getHP killerRobot

目前为止一切都很好,我不打算重写整个章节,但是我还有一个问题没有明白。接下来我们有一个函数:

damage aRobot attackDamage = aRobot (\(n,a,h) ->
                                      robot (n,a,h-attackDamage))

还有另一个函数

fight aRobot defender = damage defender attack
  where attack = if getHP aRobot > 10
                 then getAttack aRobot
                 else 0

这个程序模拟了两个机器人之间的战斗。因此,我们可以编写类似于以下内容的代码:

gentleGiant = robot ("Mr. Friendly", 10, 300)
gentleGiantRound1 = fight killerRobot gentleGiant
killerRobotRound1 = fight gentleGiant killerRobot
gentleGiantRound2 = fight killerRobotRound1 gentleGiantRound1
killerRobotRound2 = fight gentleGiantRound1 killerRobotRound1
gentleGiantRound3 = fight killerRobotRound2 gentleGiantRound2
killerRobotRound3 = fight gentleGiantRound2 killerRobotRound2

它可以正常工作。但是当我试图将其放入一个函数中,这个函数封装了这些步骤并返回最后一步的结果(实际任务略有不同),我会遇到一堆与类型系统相关的错误。以下是一个简化版本,它会导致错误:

roundFights rb1 rb2 = 
 let rb2' = fight rb1 rb2
 in fight rb2' rb1

第二个战斗会让编译器爆炸出错。所有这些函数都没有类型签名是有意的——因为它们只在书籍的介绍章节中出现,类型签名还没有被解释。

有人能建议一下出了什么问题吗?

以下是源代码:

robot (name, attack, hp) = \message -> message (name, attack, hp)

name (nm, _, _) = nm
attack (_, a, _) = a
hp (_, _, p) = p

getName r = r name
getAttack r = r attack
getHP r = r hp

setName r nm = r $ \(_, a, hp) -> robot (nm, a, hp)
setAttack r a = r $ \(nm, _, hp) -> robot (nm, a, hp)
setHP r hp = r $ \(nm, a, _) -> robot (nm, a, hp)

printRobot r = r $ \(nm, a, hp) -> nm ++ " attack:" ++ show a ++ " hp:" ++ show hp

damage r ad = r $ \(nm, a, hp) -> robot (nm, a, hp - ad)
fight atacker defender = damage defender power where
  power = if getHP atacker > 10
          then getAttack atacker
          else 0

lives = map getHP

roundFights rb1 rb2 = 
   let rb2' = fight rb1 rb2
   in fight rb2' rb1
     

rb1 = robot("Killer", 25, 200)
rb2 = robot("Slayer", 15, 200)

我得到的错误信息:

D:\Dropbox\Documents\Work\HS\GetProg\Unit 10\Robot.hs:27:18:
    Occurs check: cannot construct the infinite type:
      t8 ~ ((t7, t8, t8) -> t0) -> t0
    Expected type: ((t7, t8, t8) -> ((t7, t8, t8) -> t0) -> t0) -> t6
      Actual type: ((t7, t8, t8) -> t8) -> t6
    Relevant bindings include
      rb2' :: ((t4, t5, t5) -> t5) -> t8
        (bound at D:\Dropbox\Documents\Work\HS\GetProg\Unit 10\Robot.hs:26:8)
      rb2 :: ((t2, t3, t6) -> ((t2, t3, t6) -> t) -> t)
             -> ((t4, t5, t5) -> t5) -> t8
        (bound at D:\Dropbox\Documents\Work\HS\GetProg\Unit 10\Robot.hs:25:17)
      rb1 :: ((t7, t8, t8) -> t8) -> t6
        (bound at D:\Dropbox\Documents\Work\HS\GetProg\Unit 10\Robot.hs:25:13)
      roundFights :: (((t7, t8, t8) -> t8) -> t6)
                     -> (((t2, t3, t6) -> ((t2, t3, t6) -> t) -> t)
                         -> ((t4, t5, t5) -> t5) -> t8)
                     -> t6
        (bound at D:\Dropbox\Documents\Work\HS\GetProg\Unit 10\Robot.hs:25:1)
    In the second argument of `fight', namely `rb1'
    In the expression: fight rb2' rb1

D:\Dropbox\Documents\Work\HS\GetProg\Unit 10\Robot.hs:30:27:
    No instance for (Num t1) arising from the literal `200'
    The type variable `t1' is ambiguous
    Relevant bindings include
      rb1 :: (([Char], t1, t1) -> t) -> t
        (bound at D:\Dropbox\Documents\Work\HS\GetProg\Unit 10\Robot.hs:30:1)
    Note: there are several potential instances:
      instance Num Double -- Defined in `GHC.Float'
      instance Num Float -- Defined in `GHC.Float'
      instance Integral a => Num (GHC.Real.Ratio a)
        -- Defined in `GHC.Real'
      ...plus three others
    In the expression: 200
    In the first argument of `robot', namely `("Killer", 25, 200)'
    In the expression: robot ("Killer", 25, 200)

D:\Dropbox\Documents\Work\HS\GetProg\Unit 10\Robot.hs:31:27:
    No instance for (Num t1) arising from the literal `200'
    The type variable `t1' is ambiguous
    Relevant bindings include
      rb2 :: (([Char], t1, t1) -> t) -> t
        (bound at D:\Dropbox\Documents\Work\HS\GetProg\Unit 10\Robot.hs:31:1)
    Note: there are several potential instances:
      instance Num Double -- Defined in `GHC.Float'
      instance Num Float -- Defined in `GHC.Float'
      instance Integral a => Num (GHC.Real.Ratio a)
        -- Defined in `GHC.Real'
      ...plus three others
    In the expression: 200
    In the first argument of `robot', namely `("Slayer", 15, 200)'
    In the expression: robot ("Slayer", 15, 200)

D:\Dropbox\Documents\Work\HS\GetProg\Unit 10\Robot.hs:33:8:
    No instance for (Ord t1) arising from a use of `fight'
    The type variable `t1' is ambiguous
    Relevant bindings include
      rb2' :: (([Char], t1, t1) -> t) -> t
        (bound at D:\Dropbox\Documents\Work\HS\GetProg\Unit 10\Robot.hs:33:1)
    Note: there are several potential instances:
      instance Integral a => Ord (GHC.Real.Ratio a)
        -- Defined in `GHC.Real'
      instance Ord () -- Defined in `GHC.Classes'
      instance (Ord a, Ord b) => Ord (a, b) -- Defined in `GHC.Classes'
      ...plus 24 others
    In the expression: fight rb1 rb2
    In an equation for rb2': rb2' = fight rb1 rb2

D:\Dropbox\Documents\Work\HS\GetProg\Unit 10\Robot.hs:34:8:
    No instance for (Ord t1) arising from a use of `fight'
    The type variable `t1' is ambiguous
    Relevant bindings include
      rb1' :: (([Char], t1, t1) -> t) -> t
        (bound at D:\Dropbox\Documents\Work\HS\GetProg\Unit 10\Robot.hs:34:1)
    Note: there are several potential instances:
      instance Integral a => Ord (GHC.Real.Ratio a)
        -- Defined in `GHC.Real'
      instance Ord () -- Defined in `GHC.Classes'
      instance (Ord a, Ord b) => Ord (a, b) -- Defined in `GHC.Classes'
      ...plus 24 others
    In the expression: fight rb2' rb1
    In an equation for rb1': rb1' = fight rb2' rb1

D:\Dropbox\Documents\Work\HS\GetProg\Unit 10\Robot.hs:35:9:
    No instance for (Ord t1) arising from a use of `fight'
    The type variable `t1' is ambiguous
    Relevant bindings include
      rb2'' :: (([Char], t1, t1) -> t) -> t
        (bound at D:\Dropbox\Documents\Work\HS\GetProg\Unit 10\Robot.hs:35:1)
    Note: there are several potential instances:
      instance Integral a => Ord (GHC.Real.Ratio a)
        -- Defined in `GHC.Real'
      instance Ord () -- Defined in `GHC.Classes'
      instance (Ord a, Ord b) => Ord (a, b) -- Defined in `GHC.Classes'
      ...plus 24 others
    In the expression: fight rb1' rb2'
    In an equation for rb2'': rb2'' = fight rb1' rb2' Failed, modules loaded: none.

1
你能把你得到的错误粘贴过来吗? - nicodp
错误信息与您发布的代码不匹配(例如,错误提到了 rb1'rb2'',但在您的代码中却找不到它们)。您确定已经发布了实际的代码吗?也许您省略了某些部分? - Fyodor Soikin
我还是没有看到关于 rb1'rb2'' 的任何提及。 - Fyodor Soikin
1
我猜你的 damage 函数有问题,但我还没想出为什么(没有类型信息让我很烦:P)。你可以试试这个定义:damage aRobot attackDamage = robot (getName aRobot, getAttack aRobot, getHP aRobot - attackDamage),应该能解决问题。 - nicodp
我会尝试,但我所有的定义都是从书上拿来的 :) 如果你单独尝试damage函数,它是有效的。实际上,任务是(字面意思):编写一个threeRoundFight函数,它接受两个机器人并让它们进行三轮战斗,返回获胜者。为了避免有太多不同的变量用于机器人状态,使用一系列嵌套的lambda函数,这样你就可以只覆盖robotA和robotB。 - BarbedWire
显示剩余4条评论
1个回答

5
据我所见,您的代码在无类型环境下(例如Scheme或JavaScript)运行良好。
在类型化的环境中,它可能也能工作,但只有涉及相当复杂的类型,即排名2类型。类型推导引擎无法推导出这些类型,必须手动注释。
为了强调这一点,让我们尝试仅使用排名1类型,并添加所有注释。此部分编译正常。
type Robot a = ((String, Int, Int) -> a) -> a

robot :: (String, Int, Int) -> Robot a
robot (name, attack, hp) = \message -> message (name, attack, hp)

name :: (String, Int, Int) -> String
name (nm, _, _) = nm
attack :: (String, Int, Int) -> Int
attack (_, a, _) = a
hp :: (String, Int, Int) -> Int
hp (_, _, p) = p

getName :: Robot String -> String
getName r = r name
getAttack :: Robot Int -> Int
getAttack r = r attack
getHP :: Robot Int -> Int
getHP r = r hp

setName :: Robot (Robot a) -> String -> Robot a
setName r nm = r $ \(_, a, hp) -> robot (nm, a, hp)
setAttack :: Robot (Robot a) -> Int -> Robot a
setAttack r a = r $ \(nm, _, hp) -> robot (nm, a, hp)
setHP :: Robot (Robot a) -> Int -> Robot a
setHP r hp = r $ \(nm, a, _) -> robot (nm, a, hp)

printRobot :: Robot String -> String
printRobot r = r $ \(nm, a, hp) -> nm ++ " attack:" ++ show a ++ " hp:" ++ show hp

damage :: Robot (Robot a) -> Int -> Robot a
damage r ad = r $ \(nm, a, hp) -> robot (nm, a, hp - ad)

fight :: Robot Int -> Robot (Robot a) -> Robot a
fight atacker defender = damage defender power where
  power = if getHP atacker > 10
          then getAttack atacker
          else 0

上面的代码中,Robot a 表示一个机器人值,只能用于计算类型为a的值。例如,从 Robot Int 中可以提取攻击力和生命值,但无法提取名称。
看着这段代码...会出现很多奇怪的类型!fight 的类型非常令人困惑:
fight :: Robot Int -> Robot (Robot a) -> Robot a

第一个机器人必须进行攻击,因此它是一个Robot Int,而第二个机器人必须战斗并产生一个Robot a,因此出现了奇怪的类型Robot (Robot a)
由此我们得知,我们不能希望同时键入fight r1 r2fight r2 r1:这将需要Int = Robot a,这是不可能的。
Couldn't match typeIntwith ‘((String, Int, Int) -> a) -> a’
      Expected type: Robot (Robot a)
        Actual type: Robot Int

有什么解决方法吗?使用二阶机器人:

newtype Robot = Robot (forall a. ((String, Int, Int) -> a) -> a)

forall a 这里表示一个二阶机器人可以生成我们选择的 任何 结果,而不仅仅是一个单一的结果。因此,我们可以从一个二阶机器人中提取名称和 HP。

我们需要使用构造函数来包装/解包所有内容,这可能有点麻烦:

robot :: (String, Int, Int) -> Robot
robot (name, attack, hp) = Robot (\message -> message (name, attack, hp))
getName :: Robot -> String
getName (Robot r) = r name
-- etc.

现在,fight应该可以工作了。其他的我就留给原帖作者去尝试了。
需要注意的是,理论结果(Yoneda引理)表明我们使用的多态类型 forall a. ((String, Int, Int) -> a) -> a(String, Int, Int)等同,因此我们实际上是以一种更复杂的方式重新发明了元组。
总结:我有点惊讶于Haskell书籍提出了这个方法,这对我来说似乎是相当高级的内容。我想知道作者预期的解决方案是什么。

太棒了!真的帮了我很多。但是它并没有解释作者使用这种语言结构的一般想法,也没有任何解释。最初我使用了这种类型的同义词:type RTuple = (String, Int, Int) type Robot a = (RTuple -> a) -> a,但是它们对我没有帮助,因为我没有使用 {-# LANGUAGE RankNTypes #-} 这个想法。谢谢。 - BarbedWire
我认为这里没有任何优势。我不知道为什么书上会建议这样做。它可能是一个很好的练习,因为它迫使你使用大量的函数/延续。但是,传递延续风格已经被证明会使事情变得更加困难,只有在需要类似于callCC或其他“提前退出”机制时才值得这样做。我不会建议初学者这样做。 - chi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接