在 Haskell 中匹配明显相似的类型时出现问题

Problem matching apparently similar types in Haskell

这个小程序可以很好地处理 Int 的具体类型,但我想尝试使该函数成为具有类型约束的最通用的函数。 问题是我不明白错误信息,因为两个“刚性类型”似乎具有相同的约束,但无法匹配。

module PotentiationNew where

import Data.Bits (FiniteBits, testBit, finiteBitSize, countLeadingZeros)


potentiation :: (Num a, Integral a, FiniteBits b) => a -> b -> a -> a
potentiation base exponent modulus =
  potentiationGo $ getBits exponent
  where
    potentiationGo :: (Num c, Integral c) => [Bool] -> c
    potentiationGo [] = 1
    potentiationGo (currentExp : restExp)
      | currentExp = (x * x * base) `mod` modulus         -- This is the line with the error
      | otherwise  = (x * x)        `mod` modulus
      where x = potentiationGo restExp
      
    -- Gets a bit array represented as [Bool] (True is 1)
    -- Bit order starts with the least significant bits.
    getBits :: FiniteBits a => a -> [Bool]
    getBits x = testBit x <$> indexed
      where 
        indexed = [0 .. finiteBitSize x - countLeadingZeros x - 1]

错误信息如下:

Potentiation.hs:13:22: error:
    • Couldn't match expected type ‘c’ with actual type ‘a’
      ‘a’ is a rigid type variable bound by
        the type signature for:
          potentiation :: forall a b.
                          (Num a, Integral a, FiniteBits b) =>
                          a -> b -> a -> a
        at Potentiation.hs:6:17
      ‘c’ is a rigid type variable bound by
        the type signature for:
          potentiationGo :: forall c. (Num c, Integral c) => [Bool] -> c
        at Potentiation.hs:10:23

仅仅因为两种类型具有相同的约束并不意味着它们是同一类型。例如,IntNumIntegerNum,但是IntInteger是不一样的。

potentiationGo 中,您使用值 modulusbase,它们都是 a 类型,在您的类型签名表明应该存在的位置c 类型的值。这两种类型不一样!

解决问题的最简单方法是删除 potentiationGo 的类型签名,这应该会删除错误,但仍保持一般性(GHC 非常擅长自动派生最一般的类型)。

就是说,如果您像我一样,您真的很喜欢显式类型签名,因为至少,它可以作为一种很好的健全性检查,并使您的代码更加自文档化。在这种情况下,您真正​​想要写的是 potentiationGo :: [Bool] -> a,其中 apotentiation 本身的类型签名中的 a 相同。如果你尝试这样做,它不会起作用,因为 GHC 默认为每个签名创建新的类型变量。为了解决这个问题,您需要启用 ScopedTypeVariables,这会让 GHC 知道两个 a 是相同的。这也意味着您需要使用 forall 关键字引入类型 a(一旦您使用 forall,您需要引入类型中的每个类型变量)。它看起来像这样:

{-# LANGUAGE ScopedTypeVariables #-}

potentiation :: forall a b. (Num a, Integral a, FiniteBits b) => a -> b -> a -> a
potentiation base exponent modulus =
  potentiationGo $ getBits exponent
  where
    potentiationGo :: [Bool] -> a -- Because the `a` above is in scope and we didn't 
                                  -- write forall here, this a refers to the above one.
    ...

这是您能得到的最一般的。


请注意,这不会影响您的 getBits 函数,因为 getBits 并未直接使用值 exponent。相反,该值作为参数传递。按照这种逻辑,您还可以通过编写类似以下内容来修复 potentiationGo

potentiation base' exponent modulus' =
  potentiationGo base' modulus' $ getBits exponent
  where
    potentiationGo :: (Num c, Integral c) => c -> c -> [Bool] -> c
    potentiationGo base modulus [] = 1
    potentiationGo base modulus (currentExp : restExp) = ...

注意类型 a 的值现在如何作为参数提供给 potentiationGo。在 potentiationGo 中,值的类型为 c。类型 ca 永远不会统一为相等,但是因为 aNumIntegral,它可以传递给 potentiationGo 作为c.