为什么这个使用具有重叠实例的类型类的函数在 GHCi 中表现不同?

Why does this function using a typeclass with overlapping instances behave differently in GHCi?

背景

我在 Haskell (GHC 8.6.3) 中编写了以下代码:

{-# LANGUAGE 
  NoImplicitPrelude,
  MultiParamTypeClasses,
  FlexibleInstances, FlexibleContexts,
  TypeFamilies, UndecidableInstances,
  AllowAmbiguousTypes
#-}

import Prelude(Char, Show, show, undefined, id)

data Nil
nil :: Nil
nil = undefined

instance Show Nil where
  show _ = "nil"

data Cons x xs = Cons x xs 
  deriving Show

class FPack f r where
  fpack :: f -> r

instance {-# OVERLAPPABLE #-} f ~ (Nil -> r) => FPack f r where
  fpack f = f nil

instance (FPack (x -> b) r, f ~ (Cons a x -> b)) => FPack f (a -> r) where
  fpack f a = fpack (\x -> f (Cons a x))

此代码背后的想法是生成一个可变元数函数,该函数接受其参数并将它们打包到一个异构列表中。

例如下面的

fpack id "a" "b" :: Cons [Char] (Cons [Char] Nil)

生成列表 Cons "a" (Cons "b" nil)

问题

一般来说,我想通过传递 id 作为其 f 参数来调用 fpack(如上),因此我希望将以下函数定义为 shorthand:

pack = fpack id

如果我将上面的程序加载到 GHCi 中并执行上面的行,包将根据需要定义,其类型(由 :t 给出)为 FPack (a -> a) r => r。 所以我在我的程序中定义了这样的函数:

pack :: FPack (a -> a) r => r
pack = fpack id

但是在将上述程序加载到 GHCi 中时会出现以下错误:

bugs\so-pack.hs:31:8: error:
    * Overlapping instances for FPack (a0 -> a0) r
        arising from a use of `fpack'
      Matching givens (or their superclasses):
        FPack (a -> a) r
          bound by the type signature for:
                     pack :: forall a r. FPack (a -> a) r => r
          at bugs\so-pack.hs:30:1-29
      Matching instances:
        instance [overlappable] (f ~ (Nil -> r)) => FPack f r
          -- Defined at bugs\so-pack.hs:24:31
        instance (FPack (x -> b) r, f ~ (Cons a x -> b)) =>
                 FPack f (a -> r)
          -- Defined at bugs\so-pack.hs:27:10
      (The choice depends on the instantiation of `a0, r')
    * In the expression: fpack id
      In an equation for `pack': pack = fpack id
   |
31 | pack = fpack id
   |     

这引出了我的问题。为什么这个函数在 GHCi 中定义时有效,但在程序中定义时却不起作用?有没有办法让我在程序中正常工作?如果可以,怎么做?

我的想法

根据我对 GHC 和 Haskell 的了解,此错误是由于 pack 可以解析为两个重叠实例之一,这困扰着 GHC。但是,我认为 AllowAmbiguousTypes 选项应该通过将实例选择推迟到最终调用站点来解决该问题。不幸的是,这显然是不够的。我很好奇为什么,但我更好奇为什么 GHCi 在其 REPL 循环中接受这个定义,但在程序内部时不接受它。

一条切线

我有另一个关于这个程序的问题,它与这个问题的主旨没有直接关系,但我认为在这里问它可能是明智的,而不是针对同一个程序创建另一个问题。

如上例所示,即

fpack id "a" "b" :: Cons [Char] (Cons [Char] Nil)

我必须向 fpack 提供明确的类型签名,以便它按预期工作。如果我不提供一个(即只调用 fpack id "a" "b"),GHCi 会产生以下错误:

<interactive>:120:1: error:
    * Couldn't match type `Cons [Char] (Cons [Char] Nil)' with `()'
        arising from a use of `it'
    * In the first argument of `System.IO.print', namely `it'
      In a stmt of an interactive GHCi command: System.IO.print it

有什么办法可以改变 fpack 的定义,让 GHC 推断出正确的类型签名吗?

您需要手动实例化fpack

pack :: forall a r . FPack (a -> a) r => r
pack = fpack @(a->a) @r id

这需要ScopedTypeVariables, TypeApplications, AllowAmbiguousTypes

或者,为 id 提供类型。

pack :: forall a r . FPack (a -> a) r => r
pack = fpack (id :: a -> a)

问题是 GHC 看不到它是否应该使用 FPack (a->a) r 约束提供的 fpack。起初这可能令人费解,但请注意,如果有一些 instance FPack (T -> T) r 可用,fpack (id :: T -> T) 也可以正确生成 r。由于 id 可以同时是 a -> aT -> T(对于任何 T),GHC 不能安全地选择。

在类型错误中可以看到这种现象,因为 GHC 提到了 a0。该类型变量代表某种类型,可能是 a,但也可能是其他类型。然后可以尝试猜测为什么代码不强制 a0 = a,假装周围有其他实例可以代替。