【问题标题】:Haskell foldl cannot construct the infinite typeHaskell foldl 无法构造无限类型
【发布时间】:2020-11-11 05:36:48
【问题描述】:

我正在阅读《Get Programming with Haskell》一书:https://www.manning.com/books/get-programming-with-haskell

有一课以函数式的方式介绍OOP,以格斗机器人为例:

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

killerRobot = robot ("Kill3r",25,200)
name (n,_,_) = n
attack (_,a,_) = a
hp (_,_,hp) = hp 

getName aRobot = aRobot name
getAttack aRobot = aRobot attack
getHP aRobot = aRobot hp


setName aRobot newName = aRobot (\(n,a,h) -> robot (newName,a,h))
setAttack aRobot newAttack = aRobot (\(n,a,h) -> robot (n,newAttack,h))
setHP aRobot newHP = aRobot (\(n,a,h) -> robot (n,a,newHP))

nicerRobot = setName killerRobot "kitty"
gentlerRobot = setAttack killerRobot 5
softerRobot = setHP killerRobot 50

printRobot aRobot = aRobot (\(n,a,h) -> n ++
                                        " attack:" ++ (show a) ++
                                        " hp:"++ (show h))
                                        
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


fastRobot = robot ("speedy", 15, 40)
slowRobot = robot ("slowpoke",20,30)

fastRobotRound3 = fight slowRobotRound3 fastRobotRound2
fastRobotRound2 = fight slowRobotRound2 fastRobotRound1
fastRobotRound1 = fight slowRobotRound1 fastRobot
slowRobotRound2 = fight fastRobotRound1 slowRobotRound1
slowRobotRound3 = fight fastRobotRound2 slowRobotRound2
slowRobotRound1 = fight fastRobot slowRobot

我看到代码底部的示例战斗如何必须创建新变量来存储每次战斗后创建的对象,因为每个对象实际上只是一个存储其状态的闭包,并且闭包需要绑定到某些东西。

我很好奇是否可以使用damage 函数连续多次攻击机器人。之前的课介绍了高阶函数,包括foldl,所以我尝试多次使用它来破坏gentleGiant机器人:

pummeledGiant = foldl damage gentleGiant [100,50,100,500]

但我收到此错误:

    • Occurs check: cannot construct the infinite type:
        t1 ~ (([Char], Integer, Integer) -> t1) -> t1
      Expected type: ((([Char], Integer, Integer)
                       -> (([Char], Integer, Integer) -> t1) -> t1)
                      -> (([Char], Integer, Integer) -> t1) -> t1)
                     -> Integer
                     -> (([Char], Integer, Integer)
                         -> (([Char], Integer, Integer) -> t1) -> t1)
                     -> (([Char], Integer, Integer) -> t1)
                     -> t1
        Actual type: ((([Char], Integer, Integer)
                       -> (([Char], Integer, Integer) -> t1) -> t1)
                      -> (([Char], Integer, Integer)
                          -> (([Char], Integer, Integer) -> t1) -> t1)
                      -> (([Char], Integer, Integer) -> t1)
                      -> t1)
                     -> Integer
                     -> (([Char], Integer, Integer)
                         -> (([Char], Integer, Integer) -> t1) -> t1)
                     -> (([Char], Integer, Integer) -> t1)
                     -> t1
    • In the first argument of ‘foldl’, namely ‘damage’
      In the expression: foldl damage gentleGiant [100, 50, 100, 500]
      In an equation for ‘pummeledGiant’:
          pummeledGiant = foldl damage gentleGiant [100, 50, 100, ....]
    • Relevant bindings include
        pummeledGiant :: (([Char], Integer, Integer)
                          -> (([Char], Integer, Integer) -> t1) -> t1)
                         -> (([Char], Integer, Integer) -> t1) -> t1
          (bound at <interactive>:2:1)

我的理解是 foldl 需要 3 个参数:

  1. 二元函数
  2. 一个初始值
  3. 值列表

并以向左的方式逐步将二进制函数应用于值对,从初始值和值列表中的第一个值开始,“链接”结果,例如:

foldl (+) 0 [1..3] = ((0 + 1) + 2) + 3

我不明白我遇到的错误,也看不出我使用foldl 的方式有任何逻辑问题。我做错了什么?

【问题讨论】:

  • 说实话,这段代码我什么都看不懂。也许您应该尝试在定义中添加类型。

标签: haskell types fold


【解决方案1】:

你的直觉是对的。您对如何使用foldl 有完全正确的想法。唯一的问题是您用于机器人的类型比 Haskell 可以轻松处理的类型更复杂。您可以使用newtype 让事情安定下来,以便做到这一点。首先,这是您原始代码的相关位,其中包含类型签名和类型同义词:

{-# LANGUAGE RankNTypes #-}

type Robot = forall a. ((String, Integer, Integer) -> a) -> a

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

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

gentleGiant :: Robot
gentleGiant = robot ("Mr. Friendly", 10, 300)

pummeledGiant :: Robot
pummeledGiant = foldl damage gentleGiant [100,50,100,500]

(注意:我使用的类型实际上与推断的类型不同,因此由此产生的错误会有所不同,但根本问题是相同的。)

看看Robot如何包含一个类型变量a?这意味着它是一种多态类型。通常,当您实际使用多态类型时,Haskell 可以确定类型变量将是什么并填充它们,但在 damage 的情况下,它不能(它是 rank-2 类型)。这意味着您随后将具有多态类型的damage 放入同样具有多态类型的foldl 中。将一个多态类型放入另一个多态类型需要隐含类型,而 Haskell 还不能很好地支持。

无论如何,要修复它,您可以使用 newtype 包装器,如下所示:

{-# LANGUAGE RankNTypes #-}

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

robot :: (String, Integer, Integer) -> Robot
robot (name,attack,hp)  = MkRobot $ \message -> message (name,attack,hp)

damage :: Robot -> Integer -> Robot
damage (MkRobot aRobot) attackDamage = aRobot (\(n,a,h) ->
                                      robot (n,a,h-attackDamage))

gentleGiant :: Robot
gentleGiant = robot ("Mr. Friendly", 10, 300)

pummeledGiant :: Robot
pummeledGiant = foldl damage gentleGiant [100,50,100,500]

不过,就我个人而言,我对初学者教程使用这样的多态延续传递风格感到相当惊讶,因为它比必要的复杂得多,而且因为它会导致这样的问题。我会设计完全不同的东西,像这样:

data Robot = Robot {
  name :: String,
  attack :: Integer,
  hp :: Integer
}

robot (n,a,h) = Robot n a h

killerRobot = robot ("Kill3r",25,200)

getName = name
getAttack = attack
getHP = hp

setName aRobot newName = aRobot{ name = newName }
setAttack aRobot newAttack = aRobot{ attack = newAttack }
setHP aRobot newHP = aRobot{ hp = newHP }

nicerRobot = setName killerRobot "kitty"
gentlerRobot = setAttack killerRobot 5
softerRobot = setHP killerRobot 50

printRobot (Robot n a h) = n ++
                           " attack:" ++ (show a) ++
                           " hp:"++ (show h)
                                        
damage (Robot n a h) attackDamage = 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


fastRobot = robot ("speedy", 15, 40)
slowRobot = robot ("slowpoke",20,30)

fastRobotRound3 = fight slowRobotRound3 fastRobotRound2
fastRobotRound2 = fight slowRobotRound2 fastRobotRound1
fastRobotRound1 = fight slowRobotRound1 fastRobot
slowRobotRound2 = fight fastRobotRound1 slowRobotRound1
slowRobotRound3 = fight fastRobotRound2 slowRobotRound2
slowRobotRound1 = fight fastRobot slowRobot

pummeledGiant = foldl damage gentleGiant [100,50,100,500]

这更像是惯用的 Haskell。注意大部分代码还是一样的;它只是使用data 制作的常规类型来存储机器人,而不是奇怪的 CPS 元组。

【讨论】:

  • RankNTypes 似乎对于那些还没有读到 Haskell 简介中的第 2 单元“类型​​介绍”的人来说很难理解。
  • @amalloy 我知道,但在解释出了什么问题时似乎没有办法避免这种情况。不过,我最后更简单的返工更容易理解,希望能弥补这一点。
  • 正如@amalloy 所提到的,本书的下一部分将介绍类型。我相信这个特定示例的目的是展示如何使用闭包和一等函数以函数式风格模拟 OOP,而无需讨论类型。事实上,在本节的最后,作者明确指出“这里从头开始构建的所有功能都已经以更强大的形式存在,使用 Haskell 的类型系统。”
  • 在这一点上,我很乐意接受我试图做的事情没有奏效,因为我没有按照预期/惯用的方式去做。在我阅读了本书的下一部分之后,希望我能重新审视这个答案并更好地理解它。谢谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-10-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多