【问题标题】:Type Signature Mismatch on Bifunctor instance definitionBifunctor 实例定义上的类型签名不匹配
【发布时间】:2019-10-21 21:22:45
【问题描述】:

我正在通过阅读 Haskell Programming from First Principles, Allen & Moronuki 这本书来了解 Haskell。

在Monad Transformers, Functor & Applicative composition一章的练习中,要求读者为以下类型编写Bifunctor实例

data SemiDrei a b c = SemiDrei a

我的第一次尝试(编译)是

instance Bifunctor (SemiDrei a) where
    bimap f g (SemiDrei a) = SemiDrei a

但是,看着它,在我看来,我应该能够写成bimap f g = id,因为最后一个参数没有改变,或者写成bimap f g x = x。两者都给了我编译错误,我希望有人能向我解释为什么我不能用这些更短的替代方法来表达bimap,即为什么我必须指定(SemiDrei a)

我在 Haskell 8.6.5 上运行了这个(如果相关的话)

尝试:id

instance Bifunctor (SemiDrei a) where
    bimap f g = id

-- compile error message:
• Couldn't match type ‘a1’ with ‘b’
  ‘a1’ is a rigid type variable bound by
    the type signature for:
      bimap :: forall a1 b c d.
               (a1 -> b) -> (c -> d) -> SemiDrei a a1 c -> SemiDrei a b d
    at src/Main.hs:69:5-9
  ‘b’ is a rigid type variable bound by
    the type signature for:
      bimap :: forall a1 b c d.
               (a1 -> b) -> (c -> d) -> SemiDrei a a1 c -> SemiDrei a b d
    at src/Main.hs:69:5-9
  Expected type: SemiDrei a a1 c -> SemiDrei a b d
    Actual type: SemiDrei a b d -> SemiDrei a b d
• In the expression: id
  In an equation for ‘bimap’: bimap f g = id
  In the instance declaration for ‘Bifunctor (SemiDrei a)’
• Relevant bindings include
    f :: a1 -> b (bound at src/Main.hs:69:11)
    bimap :: (a1 -> b) -> (c -> d) -> SemiDrei a a1 c -> SemiDrei a b d
      (bound at src/Main.hs:69:5)
   |
69 |     bimap f g = id
   |                 ^^

尝试:f g x = x

instance Bifunctor (SemiDrei a) where
    bimap f g x = x

-- compile error message:
• Couldn't match type ‘a1’ with ‘b’
  ‘a1’ is a rigid type variable bound by
    the type signature for:
      bimap :: forall a1 b c d.
               (a1 -> b) -> (c -> d) -> SemiDrei a a1 c -> SemiDrei a b d
    at src/Main.hs:69:5-9
  ‘b’ is a rigid type variable bound by
    the type signature for:
      bimap :: forall a1 b c d.
               (a1 -> b) -> (c -> d) -> SemiDrei a a1 c -> SemiDrei a b d
    at src/Main.hs:69:5-9
  Expected type: SemiDrei a b d
    Actual type: SemiDrei a a1 c
• In the expression: x
  In an equation for ‘bimap’: bimap f g x = x
  In the instance declaration for ‘Bifunctor (SemiDrei a)’
• Relevant bindings include
    x :: SemiDrei a a1 c (bound at src/Main.hs:69:15)
    f :: a1 -> b (bound at src/Main.hs:69:11)
    bimap :: (a1 -> b) -> (c -> d) -> SemiDrei a a1 c -> SemiDrei a b d
      (bound at src/Main.hs:69:5)
   |
69 |     bimap f g x = x
   |                   ^

【问题讨论】:

    标签: haskell types bifunctor


    【解决方案1】:

    最后一个参数实际上并没有改变:它的类型改变了。输入为SemiDrei a x y,输出为SemiDrei a p q,其中f :: x -> pg :: y -> q

    这意味着您必须解构原始类型的值并重构新类型的值,这就是您在原始实现中所做的。

    但是您的直觉是正确的:这两个值确实具有相同的内存表示。而 GHC 可以推导出这个事实,当它推导出来的时候,它会自动为你解决一个Coercible 约束,也就是说你可以使用coerce 函数将一个转换为另一个:

     bimap _ _ = coerce
    

    【讨论】:

      【解决方案2】:

      这在更简单的情况下显示了相同的问题:

      data T a = K
      
      foo :: T a -> T b
      foo K = K  -- type checks
      
      bar :: T a -> T b
      bar x = x  -- type error
      
      -- bar = id would also be a type error, for the same reason
      

      这里的问题是foo 值中的两个K 隐藏了它们的类型参数。更准确的定义是

      -- pseudo code
      foo (K @a) = K @b
      

      在这里您可以看到隐式类型参数发生了变化。当我们在foo 的定义中写入K 时,GHC 会自动为我们推断这些类型参数。 由于它们是隐式的,它们看起来就好像它们是相同的 Ks,但它们不是类型检查器。

      相反,当我们在bar 的定义中使用x 时,没有隐式类型参数可以推断。我们有x :: T a,仅此而已。我们不能使用x 并声明具有不同类型的T b

      最后,请注意,使用“安全强制”我们可以执行直观正确的id 类型,它将一个K(一种类型)转换为另一种类型的K

      import Data.Coerce
      
      baz :: T a -> T b
      baz = coerce
      

      这是否更好是有争议的。对于简单的情况,模式匹配比coerce 更容易理解,因为后者可以执行大量(安全)强制转换,可能让读者猜测类型级别的实际情况。

      【讨论】:

        【解决方案3】:

        关键在于bimap的类型签名:

        bimap :: Bifunctor p => (a -> b) -> (c -> d) -> p a c -> p b d
        

        在这种特殊情况下,如果我们将 p 特化为 SemiDrei a 并重命名类型变量以避免与 a 混淆,我们会得到:

        bimap :: (b -> c) -> (d -> e) -> SemiDrei a b d -> SemiDrei a c e
        

        所以当你尝试实现这个时:

        bimap f g = ...
        

        函数fg 是完全任意的,不仅在它们的实现上,而且在它们的输入和返回类型上也是如此。 f 具有 b -> c 类型,其中 bc 绝对可以是任何东西——g 也是如此。您给出的定义必须适用于调用者提供的任何类型和函数——这就是(参数)多态的含义。

        如果我们现在用这些术语来看看你的三个定义,我们就能解开这个明显的谜团:

        第一:

        bimap f g (SemiDrei a) = SemiDrei a
        

        正如您所见,这完全没问题。 SemiDrei a 的类型为 SemiDrei a b c,其中仅指定了 a。这意味着它可以采用任何类型,例如SemiDrei a Int String,或SemiDrei [Bool] (Char, [Double]),或其他任何类型。 SemiDrei a 本身是多态的,它可以是任何兼容的类型。这意味着它尤其可以作为bimap上述签名中的SemiDrei a b cSemiDrei a c e

        与您的其他尝试对比:

        bimap f g = id
        

        这里的问题是id 虽然是多态的,但对于这个目的来说,多态还不够。它的类型是a -> a(对于任何a),特别是可以专门化为SemiDrei a b c -> SemiDrei a b c。但它不可能根据需要专门用于 SemiDrei a b d -> SemiDrei a c e 类型,因为 bcde 通常是完全不同的类型。回想一下bimap调用者 可以选择类型是什么——他们可以轻松选择函数fg 其中bc 是不同的类型,例如,然后id 无法将SemiDrei a b d 转换为SemiDrei a c e,因为它们是不同的类型。

        在这个阶段你可能会反对SemiDrei a 的值可以是所有这些类型的值。这是完全正确的,但它与类型推断无关——编译器只关心类型,而不关心它们可能包含哪些值。它必须考虑到不同的类型具有完全不同的、不相交的值。而且,比如说,SemiDrei a Int StringSemiDrei a Bool Char 实际上是不同的类型。同样,编译器不知道 Int 等实际上并没有被任何类型的值使用。这确实是为什么在实践中使用这种“幻像类型”(出现在类型定义中但不在其任何数据构造函数中的类型)的原因 - 以允许编译器能够按类型区分它们,即使运行时表示可能是完全等价的。

        至于您的第三次尝试 bimap f g x = x,这与前一次完全相同 - 它限制 bimap f g 使其输出类型与其输入相同。 (实际上完全等同于bimap f g = id。)

        所以重要的一点是,在类型检查阶段,编译器只关心类型 - 两种具有不同名称的类型被(并且必须)认为是完全不同的,即使等效值可能嵌入两者中。

        【讨论】:

          猜你喜欢
          • 2012-07-07
          • 2020-07-28
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-10-02
          • 1970-01-01
          相关资源
          最近更新 更多