如何将 TypeApplications 与 typeclass 方法一起使用,为什么 GHCi 会推断出我不能使用的类型?



摘要

我有一个类型类,我想为它写一些"通用术语"。我有两个问题:

  1. 使用:t向 GHCi 询问通用术语的类型有效,但使用该推断类型失败 - 为什么?
  2. 如何将TypeApplications与类型类的方法一起使用?

我正在使用GHC 8.8.4。对于这两个问题,我有以下示例Main.hs包含一个类型类F和类型Empty,它是F的实例。

{-# LANGUAGE NoStarIsType #-}
{-# LANGUAGE PolyKinds    #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
module Main where
import GHC.Types (Type)
class F (f :: k -> Type) where
type Plus f (a :: k) (b :: k) :: k
zero :: f a
plus :: f a -> f b -> f (Plus f a b)
data Empty (a :: Type) = Empty
instance F Empty where
type Plus Empty a b = (a, b)
zero     = Empty
plus _ _ = Empty

1. 推断类型不起作用?

我想构造一个类型类F的通用术语。例如,plus zero zero. 当我向GHCi询问这个术语的类型时,它给了我我所期望的:

*Main> :t plus zero zero
plus zero zero :: F f => f (Plus f a b)

令人惊讶的是,如果我尝试分配此术语,则会出现错误。也就是说,如果我将以下内容添加到Main.hs

-- This doesn't work.
plusZero :: F f => f (Plus f a b)
plusZero = plus zero zero

在 GHCi 中重新加载文件会抱怨错误:

• Couldn't match type ‘Plus f a0 b0’ with ‘Plus f a b’
Expected type: f (Plus f a b)
Actual type: f (Plus f a0 b0)
NB: ‘Plus’ is a non-injective type family
The type variables ‘a0’, ‘b0’ are ambiguous
• In the expression: plus zero zero
In an equation for ‘plusZero’: plusZero = plus zero zero

我的第一个问题是:为什么GHCi似乎推断出类型,但在我明确注释术语时却拒绝它?

2. 使用TypeApplications而不是注释

我可以通过注释zero术语的类型来解决第一个问题:

-- This works
plusZero1 :: forall f a b . F f => f (Plus f a b)
plusZero1 = plus (zero :: f a) (zero :: f b)

但是,当条款变大时,这有点笨拙。我想做的是使用TypeApplications.我试过这个:

-- This doesn't work
plusZero2 :: forall f a b . F f => f (Plus f a b)
plusZero2 = plus @f @a @b zero zero

但GHCi抱怨:

• Expecting one more argument to ‘f’
Expected a type, but ‘f’ has kind ‘k -> *’
• In the type ‘f’
In the expression: plus @f @a @b zero zero
In an equation for ‘plusZero2’: plusZero2 = plus @f @a @b zero zero
• Relevant bindings include
plusZero2 :: f (Plus f a b) (bound at Main.hs:36:1)

奇怪的是,如果我首先定义其他函数plus'并按如下方式zero',一切都按预期工作:

zero' :: forall f a . F f => f a
zero' = zero
plus' :: forall f a b . F f => f a -> f b -> f (Plus f a b)
plus' = plus
-- This works fine
plusZero3 :: forall f a b . F f => f (Plus f a b)
plusZero3 = plus' @f @a @b zero' zero'

所以似乎我不明白TypeApplications如何使用类型类方法。 如何使用具有pluszero的类型应用程序,而无需定义附加函数plus'zero'

  1. 推断类型不起作用?

在您的示例中,GHC 确实可以推断类型,但它不能接受您的签名。这似乎违反直觉,但如果您考虑总体情况,它确实是有道理的。

Plus f a b是非注射型家族。对于GHC在类型检查时知道的所有信息,它可以定义为对所有fabPlus f a b = a

假设我们已经定义了一个术语(为了清楚起见,我添加了foralls)

foo :: forall f a b. F f => f (Plus f a b)

我们写

bar :: forall f a b. F f => f (Plus f a b)
bar = foo

这不应该键入检查 (!),因为它本质上是不明确的。程序员,作为一个人,可能会期望编译器推断这些类型:

bar :: forall f a b. F f => f (Plus f a b)
bar = foo @f @a @b

但是,可能还有其他正确的推断类型!事实上,如果如上所述定义Plus,这也将类型检查:

bar :: forall f a b. F f => f (Plus f a b)
bar = foo @f @a @String

使用它,foo将生成与f (Plus f a b)相同的f (Plus f a String),因此所有类型都进行检查。由于程序员可能打算使用@b以外的其他东西,我们在这里停止报告类型错误的歧义。


从技术上讲,推理过程中发生的情况是这样的:对 poltmorphicfoo的调用链接到新的未知类型变量:

bar :: forall f a b. F f => f (Plus f a b)
bar = foo @xf @xa @xb

然后,统一发生:xf (Plus xf xa xb)foo @xf @xa @xb的类型,并与提供的签名统一以查找未知数:

xf (Plus xf xa xb) ~ f (Plus f a b)

由此我们应用统一算法:

xf ~ f
Plus xf xa xb ~ Plus f a b

所以我们找到了未知xf的类型,并代入我们得到:

xf ~ f
Plus f xa xb ~ Plus f a b

但是,我们到此为止。我们无法推断xa ~ axb ~ b,因为类型族不是单射的。


  1. 使用类型应用程序而不是注释

问题是有一个隐藏的@k参数,因为它发生在类中。 使用:t +v显示具有所有forall的真实类型:

> :t +v plus
plus
:: forall k (f :: k -> *) (a :: k) (b :: k).
F f =>
f a -> f b -> f (Plus f a b)

传递@k也有效:

plusZero2 :: forall k (f :: k -> Type) a b . F f => f (Plus f a b)
plusZero2 = plus @k @f @a @b zero zero

或者,让编译器推断出@k

plusZero2 :: forall f a b . F f => f (Plus f a b)
plusZero2 = plus @_ @f @a @b zero zero

相关内容

最新更新