为什么使用带有重叠实例的类型类的此函数在GHCi中的行为会有所不同?

时间:2019-05-20 20:04:57

标签: haskell ghc typeclass ghci overlapping-instances

背景

我已经在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))

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

例如,以下

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

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

问题

通常,我想通过传递fpack作为其id参数来调用f(如上所述),因此我希望将以下函数定义为速记:

pack = fpack id

如果我将上面的程序加载到GHCi中并执行上面的行,则pack根据需要定义,其类型(由: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推断出正确的类型签名吗?

1 个答案:

答案 0 :(得分:3)

您需要手动实例化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约束所提供的FPack (a->a) r的问题。乍一看可能令人困惑,但是请注意,如果有fpack (id :: T -> T)可用,r也可以正确生成instance FPack (T -> T) r。由于id可以同时是a -> aT -> T(对于任何T),因此GHC无法安全选择。

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