这种涉及反射的神秘Haskell类型错误的原因是什么?

时间:2017-12-08 07:01:27

标签: haskell

我正在使用Haskell reflection包,我遇到了一个我不完全理解的类型错误。首先,我尝试编写以下函数,很高兴地进行了类型检查:

{-# LANGUAGE TypeApplications #-}

reifyGood :: (forall s. Reifies s a => Proxy a) -> ()
reifyGood p = reify undefined (\(_ :: Proxy t) -> p @t `seq` ())

有趣的是,这个略有不同的程序类型检查:

reifyBad :: (forall s. Reifies s s => Proxy s) -> ()
reifyBad p = reify undefined (\(_ :: Proxy t) -> p @t `seq` ())

• Could not deduce (Reifies s s) arising from a use of ‘p’
  from the context: Reifies s a0
    bound by a type expected by the context:
               Reifies s a0 => Proxy s -> ()
• In the first argument of ‘seq’, namely ‘p @t’
  In the expression: p @t `seq` ()
  In the second argument of ‘reify’, namely
    ‘(\ (_ :: Proxy t) -> p @t `seq` ())’

表达式是相同的,但请注意类型签名之间的区别:

reifyGood :: (forall s. Reifies s a => Proxy a) -> ()
reifyBad  :: (forall s. Reifies s s => Proxy s) -> ()

我觉得这很好奇。乍一看,这是无效的,因为在第二个例子中,skolem s将逃避其范围。然而,事实并非如此 - 错误消息从未提及skolem逃脱,与此略有不同的程序形成鲜明对比:

reifyBad' :: (forall s. Reifies s s => Proxy s) -> ()
reifyBad' p = reify undefined (\(_ :: Proxy t) -> p @t) `seq` ()

• Couldn't match expected type ‘t0’ with actual type ‘Proxy s’
    because type variable ‘s’ would escape its scope
  This (rigid, skolem) type variable is bound by
    a type expected by the context:
      Reifies s a0 => Proxy s -> t0
• In the expression: p @t
  In the second argument of ‘reify’, namely
    ‘(\ (_ :: Proxy t) -> p @t)’
  In the first argument of ‘seq’, namely
    ‘reify undefined (\ (_ :: Proxy t) -> p @t)’

所以也许其他东西在这里发挥作用。

检查reify的类型,有点不清楚:

reify :: forall a r. a -> (forall s. Reifies s a => Proxy s -> r) -> r

as的范围明显不同,因此GHC不允许它们统一似乎是有道理的。但是,似乎关于Reifies的函数依赖引入的局部等式约束会引起某种奇怪的行为。有趣的是,这对功能是强大的:

foo :: forall a r. Proxy a -> (forall s. (s ~ a) => Proxy s -> r) -> r
foo _ f = f Proxy

bar :: (forall a. Proxy a) -> ()
bar p = let p' = p in foo p' (\(_ :: Proxy s) -> (p' :: Proxy s) `seq` ())

...但是删除foo类型签名中的等式约束会使它们无法进行类型检查,从而产生一个skolem转义错误:

• Couldn't match type ‘a0’ with ‘s’
    because type variable ‘s’ would escape its scope
  This (rigid, skolem) type variable is bound by
    a type expected by the context:
      Proxy s -> ()
  Expected type: Proxy s
    Actual type: Proxy a0
• In the first argument of ‘seq’, namely ‘(p' :: Proxy s)’
  In the expression: (p' :: Proxy s) `seq` ()
  In the second argument of ‘foo’, namely
    ‘(\ (_ :: Proxy s) -> (p' :: Proxy s) `seq` ())’

此时,我很困惑。我有几个(高度相关的)问题。

  1. 为什么reifyBad首先未能进行类型检查?

  2. 更具体地说,为什么会产生缺少实例错误?

  3. 此外,这种行为是期望的还是定义明确的,还是仅仅是类型检测器的一个奇怪的边缘情况恰好产生了这个特定的结果?

1 个答案:

答案 0 :(得分:6)

reify :: forall a r. a -> (forall s. Reifies s a => Proxy s -> r) -> r

skolem要求基本上表明上述类型中的r不能依赖于在第二个参数中量化的s。否则,实际上它将“逃避”其范围,因为reify返回r

reifyBad :: (forall s. Reifies s s => Proxy s) -> ()
reifyBad p = reify undefined (\(_ :: Proxy t) -> p @t `seq` ())

我们看到reify的第二个参数是\(_ :: Proxy t) -> p @t `seq` (),因此类型r将是该函数的返回类型,即()。由于r ~ ()不依赖s,因此此处没有任何转义问题。

但是,p @t根据p的类型需要Reifies t t。 由于reify会选择t ~ s,因此约束与Reifies s s相同。 相反,reify仅提供Reifies s a,其中aundefined的类型。

这里的细微之处在于,虽然undefined可以生成任何类型a,但类型检查器无法统一sa。这是因为与reify具有相同类型的函数有权仅接收固定(刚性)类型a的一个值,然后根据需要选择尽可能多的类型s。将所有此类s与单个a统一起来是错误的,有效地限制s选择reify

相反,在变体中

reifyBad' :: (forall s. Reifies s s => Proxy s) -> ()
reifyBad' p = reify undefined (\(_ :: Proxy t) -> p @t) `seq` ()

此处r被推断为\(_ :: Proxy t) -> p @t的返回类型,即Proxy t,其中t ~ s。由于r ~ Proxy s确实取决于s,因此我们会触发一个skolem错误。