当我指定x的类型为a时,为什么Haskell尝试推断它的类型为a0?

时间:2018-07-16 21:12:51

标签: haskell types

有时候,我会在签名中指定某种类型,例如a,GHC会回应说它无法推断其类型为a0。发生这种情况的原因是单一的还是多种可能的原因?有时我解决了,有时没有解决;我希望有一个统一的理论。

这是一个简短的例子。 (要查看此代码,包括解释其工作方式的注释,请参见here。)

{-# LANGUAGE MultiParamTypeClasses
           , AllowAmbiguousTypes
           , FlexibleInstances
           , GADTs #-}

type SynthName = String

data Synth format where
  Synth :: SynthName -> Synth format

data MessageA format where
  MessageA :: String -> MessageA format
data MessageB format where
  MessageB :: String -> MessageB format

class (Message format) a where
  theMessage :: a -> String
instance (Message format) (MessageA format) where
  theMessage (MessageA msg) = msg
instance (Message format) (MessageB format) where
  theMessage (MessageB msg) = msg

play :: Message format m => Synth format -> m -> IO ()
play (Synth name) msg =
  print $ name ++ " now sounds like " ++ theMessage msg

这会产生以下错误。

riddles/gadt-forget/closest-to-vivid.hs:38:42: error:
    • Could not deduce (Message format0 m)
        arising from a use of ‘theMessage’
      from the context: Message format m
        bound by the type signature for:
                   play :: forall format m.
                           Message format m =>
                           Synth format -> m -> IO ()
        at riddles/gadt-forget/closest-to-vivid.hs:36:1-54
      The type variable ‘format0’ is ambiguous
      Relevant bindings include
        msg :: m (bound at riddles/gadt-forget/closest-to-vivid.hs:37:19)
        play :: Synth format -> m -> IO ()
          (bound at riddles/gadt-forget/closest-to-vivid.hs:37:1)
      These potential instances exist:
        instance Message format (MessageA format)
          -- Defined at riddles/gadt-forget/closest-to-vivid.hs:30:10
        instance Message format (MessageB format)
          -- Defined at riddles/gadt-forget/closest-to-vivid.hs:32:10
    • In the second argument of ‘(++)’, namely ‘theMessage msg’
      In the second argument of ‘(++)’, namely
        ‘" now sounds like " ++ theMessage msg’
      In the second argument of ‘($)’, namely
        ‘name ++ " now sounds like " ++ theMessage msg’
   |
38 |   print $ name ++ " now sounds like " ++ theMessage msg

2 个答案:

答案 0 :(得分:6)

Message是一个多参数类型类。为了确定使用哪个实例,需要为a format进行具体选择。但是,方法

theMessage :: a -> String

甚至都没有提到format,因此我们无法确定要使用哪种具体类型来查找Message的实例。您大概得到的模棱两可的类型错误就是与此有关的(但是这可能是一个棘手的错误消息,我不怪您仅启用了扩展名。)

快速解决方案是使用formatScopedTypeVariables(或向TypeApplications添加Proxy format参数)手动指定theMessage变量。

play :: forall format m. Message format m => Synth format -> m -> IO ()
play (Synth name) msg =
    print $ name ++ " now sounds like " ++ theMessage @format msg

但是,Message类由于滥用类型类而引发了红旗。这并不总是很糟糕,但是每当您看到一个类的方法都具有类似

类型的类时,
:: a -> Foo
:: a -> Bar

即它们在一个不变的位置上占据一个a,很可能根本不需要类型类。将类转换为数据类型通常更清洁,就像这样:

data Message format = Message { theMessage :: String }

其中每个方法都成为一个记录字段。然后,实例化的具体类型(例如MessageA)将“降级”到函数中:

messageA :: String -> Message format
messageA msg = Message { theMessage = msg }

只要您通过a约束传递Message,就只需传递Message即可。 a变成虚无。

完成这个因素后,您可能会注意到,您写的很多内容都是重言式的和不必要的。好!去掉它!

答案 1 :(得分:3)

当涉及多态绑定的类型检查代码时,类型推断引擎会为绑定的每次使用创建新的类型变量。

这是一个具体的例子:

show () ++ show True

现在,我们知道show :: Show a => a -> String。在第一个调用show的上方选择了a ~ (),第二个选择了a ~ Bool。等待!由于()Bool是不同的类型,因此这看起来是矛盾的,因此它们不能都等于a。是吗?

不,这并不矛盾...显然,show的每个调用都可以独立选择a。在类型推断过程中,大致如下所述。

对于每个调用,我们通过重命名手边多态类型中的通用量化类型变量来生成一个新的类型变量

-- for the first call
show :: Show a0 => a0 -> String
-- for the second call
show :: Show a1 => a1 -> String

然后,我们只需选择a0 ~ ()a1 ~ Bool就可以了。用户从未意识到这是在幕后进行的。

但是,如果存在类型错误,可以将新生成的变量报告给用户,从而揭示出一部分基础推理算法。例如

show []

在这里,使用了两个多态值,因此我们为这两个值生成了新的变量。

[] :: [a0]
show :: Show a1 => a1 -> String

要进行类型检查,我们需要a1 ~ [a0],所以最后(在进行了一些上下文缩减之后,现在已经不重要了):

[] :: [a0]
show :: Show a0 => [a0] -> String

很好,我们不再有a1。但是a0呢?我们没有找到a0的任何特定值。确实,我们不能这样做,因为代码不包含任何强制选择的内容:a0最后仍然是模棱两可的类型。

之所以会这样,是因为[]可以创建任何类型的列表,而show可以将任何类型的列表作为输入(只要其元素类型是可显示的)。但是这些限制并不能告诉我们类型应该是什么!

最后,代码不明确,因此我们必须通过告诉GHC我们选择哪种类型来对其进行修复。这些都可以

show ([] :: [Int])   -- choose a0 ~ Int
show ([] :: [Bool])  -- choose a0 ~ Bool
show ([] :: [Char])  -- choose a0 ~ Char

在您的代码中

play :: Message format m => Synth format -> m -> IO ()
play (Synth name) msg =
  print $ name ++ " now sounds like " ++ theMessage msg

没有任何东西可以迫使theMessage msg使用与format类型相同的play。对您来说也许应该是“显而易见的”,但这并不是唯一的选择。

在这里选择相同的format是很棘手的,因为您的class的类型不明确。仍然可以通过打开TypeApplciationsAmbiguousTypes来使用此方法,但是有一些情况告诉我您的设计可能是错误的,因此在这里我建议谨慎一点。您正在努力实现什么?为什么the Message的类型没有以任何方式提及format