为什么高阶函数返回了这个(意料之外)的类型?

5

混淆于Haskell类型 [*]

我正在开始学习Haskell,并对Haskell的类型推断结果感到困惑(如下所示的示例)。不幸的是,我的Haskell水平还不够熟练,无法用适当的方式表达真正的问题,只能通过示例进行工作。

[*] 一旦我知道真正的问题,我会更新说明..

我的目标

我正在跟随《使用Haskell进行编程》书的第10课中介绍的一种“保持状态”的方法:


-- The result of calling robot (`r`) is a function that takes 
-- another function (`message` as argument). The parameters 
-- passed to the initial call to `robot` are treated 
-- as state and can be passed to `message`.
-- 
robot name health attack = \message -> message name health attack

getName r = r (\name _ _ -> name)

klaus = robot "Klaus" 50 5

*Main> getName klaus
"Klaus"

我理解了其要义并尝试创建一个小型机器人战斗。最终,我想要像这样的东西:

klaus = robot "Klaus" 50 5
peter = robot "Peter" 50 5

victor = fight klaus peter

getName victor
-- should be "Klaus"

我的机器人

以下是我编写的实现:

robot name health attack = \message -> message name health attack

isAlive r = r (\_ health _ -> health > 0)

fight attacker defender = if isAlive attacker then
                             attacker
                          else
                             defender



printRobot r = r (\name health  attack -> "Name: " ++ (show name) ++", health: " ++ (show health) ++ ", attack: " ++ (show attack))

klaus = robot "Klaus" 50 5
peter = robot "Peter" 60 7

在ghci中进行实验

该代码可以在ghci中加载 (:l robots.hs)。当我试图测试我的代码时,发现事情并不完全按照计划进行:类型系统和我似乎对结果类型的看法不太一样。

请指出我的推理错误之处,并帮助我理解我的错误:

--
-- in ghci
--

*Main> :t klaus
-- I understood: 
-- klaus is a function. I have to pass a function that
-- takes name, health, and attack as parameters and 
-- returns something of type "t". 
--
-- A result of same type "t" is then returned by calling klaus
klaus :: ([Char] -> Integer -> Integer -> t) -> t

-- check the "isAlive" function:
-- As expected, it returns a Bool
*Main> :t isAlive klaus
isAlive klaus :: Bool

-- This is also expected as klaus has health > 0
*Main> isAlive klaus
True

-- Inspecting the type of `isAlive` confuses me:
--
-- I do understand:
--
-- The first parameter is my "robot". It has to accept a function
-- that returns a boolean (basically the isAlive logic):
--
-- (t1 -> a -> t -> Bool) 
-- - t1: name, ignored
-- - a: health, needs to be a comparable number 
-- - t: attack value, ignored
-- - returns boolean value if the health is >0
--
-- What I do NOT understand is, why doesn't it have the following type
-- isAlive :: (Ord a, Num a) => (t1 -> a -> t -> Bool) -> Bool
*Main> :t isAlive
isAlive :: (Ord a, Num a) => ((t1 -> a -> t -> Bool) -> t2) -> t2

-- The signature of `isAlive` bites me in my simplified 
-- fight club:
-- If the attacker is alive,  the attacker wins, else 
-- the defender wins:
fight attacker defender = if isAlive attacker then
                 attacker
              else
                 defender

-- I would expect the "fight" function to return a "robot".
-- But it does not:
*Main> victor = fight klaus peter
*Main> :t victor
victor :: ([Char] -> Integer -> Integer -> Bool) -> Bool

*Main> printRobot klaus
"Name: \"Klaus\", health: 50, attack: 5"
*Main> printRobot peter
"Name: \"Peter\", health: 60, attack: 7"
*Main> printRobot victor 

<interactive>:25:12: error:
    • Couldn't match type ‘[Char]’ with ‘Bool
      Expected type: ([Char] -> Integer -> Integer -> [Char]) -> Bool
        Actual type: ([Char] -> Integer -> Integer -> Bool) -> BoolIn the first argument of ‘printRobot’, namely ‘victor’
      In the expression: printRobot victor
      In an equation for ‘it’: it = printRobot victor

这些问题

  • isAlive的签名为什么不是(t1 -> a -> t -> Bool) -> Bool
  • fight函数有什么问题?

我所学到的

根据我目前的理解,我无法修复问题,但现在(多亏了@chi的出色答案),我可以理解问题。

为了帮助其他遇到同样问题的初学者,这里有一个简化版本的问题及我的推理过程:

  • 我通过buildSSIclosure构建一个闭包,其中包含两个字符串s1s2和一个整数i1。通过“发送消息”(传递函数)到闭包中,我可以访问闭包的“状态”。
  • 我可以编写简单的访问器getS1getS2getI1
  • 我想编写一个函数,它接受一个ssiClosure并通过访问器获取Int[Char]属性。
-- IMPORTANT: the return value `t` is not bound to a specific type
buildSSIclosure :: [Char] -> [Char] -> Int -> ([Char] -> [Char] -> Int -> t) -> t
buildSSIclosure s1 s2 i1 = (\message -> message s1 s2 i1)
buildSSIclosure的定义中有t未绑定。当使用访问器中的任何一个时,ssiClosure实例的t将会绑定到一个类型。
getS1 :: (([Char] -> [Char] -> Int -> [Char]) -> [Char]) -> [Char]
getS2 :: (([Char] -> [Char] -> Int -> [Char]) -> [Char]) -> [Char]
getI1 :: (([Char] -> [Char] -> Int -> Int) -> Int) -> Int

-- `t` is bound to [Char]
getS1 ssiClosure = ssiClosure (\ s1 _ _ -> s1)

-- `t` is bound to [Char]
getS2 ssiClosure = ssiClosure (\ _ s2 _ -> s2)

-- `t` is bound to int
getI1 ssiClosure = ssiClosure (\ _ _ i1 -> i1)

我直接访问了lambda函数调用的两个参数。这样做可以绑定t[Char]

getS1I1_direct ssiClosure = ssiClosure (\ s1 _ i1 -> s1 ++ ", " ++ show i1)

调用两个字符串访问器

我可以通过这些访问器访问S1S2。这是因为getS1getS2会将tssiClosure绑定到[Char]:

getS1S2_indirect ssiClosure = show (getS1 ssiClosure) ++ ", " ++ show(getS2 ssiClosure)

访问Char和Int

接下来的步骤是访问int和string属性。但这样做是无法编译通过的!

这是我的理解:

  • 调用getS1需要将闭包中的t绑定到[Char]
  • 调用getI1需要将闭包中的t绑定到Int

它不能同时绑定到两者,因此编译器告诉我这个错误:

getS1I1_indirect ssiClosure = show(getS1 ssiClosure) ++ ", "  ++ show(getI1 ssiClosure)

    • Couldn't match type ‘[Char]’ with ‘Int
      Expected type: ([Char] -> [Char] -> Int -> Int) -> Int
        Actual type: ([Char] -> [Char] -> Int -> [Char]) -> [Char]In the first argument of ‘getI1’, namely ‘ssiClosure’
      In the first argument of ‘show’, namely ‘(getI1 ssiClosure)’
      In the second argument of ‘(++)’, namely ‘show (getI1 ssiClosure)’

我仍然没有通过查看错误来识别问题的技能。但是还有希望 ;-)

1个回答

5
为什么 isAlive 的签名不是 (t1 -> a -> t -> Bool) -> Bool
isAlive r = r (\_ health _ -> health > 0)

让我们从lambda开始。我认为你可以看到

(\_ health _ -> health > 0) :: a -> b -> c -> Bool

在这里,变量b必须同时属于Ord类(用于>)和Num类(用于0)。

由于参数r接受Lambda作为输入,因此它必须是一个接受Lambda作为输入的函数:

r :: (a -> b -> c -> Bool) -> result

最后,isAlive 接受 r 作为参数,并返回与 r 相同的结果。因此:

isAlive :: ((a -> b -> c -> Bool) -> result) -> result

添加约束条件和稍微更改一下类型变量的名称后,我们得到了GHCi的类型:

isAlive :: (Ord a, Num a) => ((t1 -> a -> t -> Bool) -> t2) -> t2

请注意,此类型比此类型更通用:
isAlive :: (Ord a, Num a) => ((t1 -> a -> t -> Bool) -> Bool) -> Bool

这句话大致意思是“给我一个生成Bool的机器人,我就给你一个Bool”。

我的fight函数有什么问题?

fight attacker defender = if isAlive attacker then
                 attacker
              else
                 defender

这很棘手。上面的代码调用了isAlive attacker,这迫使attacker拥有类型(a -> b -> c -> Bool) -> result。然后,result必须是Bool,因为它被用在了if语句中。此外,这使得defender具有与attacker相同的类型,因为if then else的两个分支必须返回相同类型的值。
因此,fight的输出必须是一个“生成布尔值的机器人”,即一个机器人不能再生成其他类型的值。
这可以使用二阶类型来修复,但如果您是初学者,我不建议现在尝试这样做。对于初学者来说,这个练习看起来非常高级,因为有许多lambda表达式在传递。
更具体地说,您正在到处传递Church编码元组,这只能完全使用二阶多态性工作。传递一阶元组会更简单。
无论如何,这里是一个可能的修复方法。这将打印Klaus作为获胜者。
{-# LANGUAGE Rank2Types #-}

isAlive :: (Ord h, Num h) => ((n -> h -> a -> Bool) -> Bool) -> Bool
isAlive r = r (\_ health _ -> health > 0)

-- A rank-2 polymorphic robot, isomorphic to (n, h, a)
type Robot n h a = forall result . (n -> h -> a -> result) -> result

fight :: (Ord h, Num h) => Robot n h a -> Robot n h a -> Robot n h a
fight attacker defender = if isAlive attacker
   then attacker
   else defender

robot :: n -> h -> a -> Robot n h a
robot name health attack = \message -> message name health attack

printRobot :: (Show n, Show h, Show a) => ((n -> h -> a -> String) -> String) -> String
printRobot r = r (\name health  attack -> 
   "Name: " ++ show name ++
   ", health: " ++ show health ++
   ", attack: " ++ show attack)

klaus, peter :: Robot String Int Int
klaus = robot "Klaus" 50 5
peter = robot "Peter" 60 7

main :: IO ()
main = do
   let victor = fight klaus peter
   putStrLn (printRobot victor)

最后一条建议

我建议您为每一个顶层函数添加类型信息。虽然Haskell可以推断出这些类型,但是让程序员手头有类型信息非常方便。此外,如果您写下您希望拥有的类型,GHC将会对其进行检查。通常情况下,GHC会推断出程序员不打算使用的类型,使代码看起来很好,实际上却存在问题。这通常会导致程序后期产生令人困惑的类型错误,因为推断出的类型与代码其他部分不匹配。


1
我认为现在RankNTypesRank2Types更受欢迎。 - bradrn
@chi,你的回答非常有用,让我理解了这个问题。谢谢!我已经更新了问题,让其他人也能从中获得新的知识 :-) - Jens

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