了解具有类约束的rank 2类型别名

时间:2013-01-29 14:28:31

标签: haskell types typeclass higher-rank-types

我的代码经常使用看起来像

的函数
foo :: (MyMonad m) => MyType a -> MyOtherType a -> ListT m a

为了缩短这个,我写了以下类型的别名:

type FooT m a = (MyMonad m) => ListT m a

GHC让我打开Rank2Types(或RankNTypes),但当我使用别名将我的代码缩短为

时,我没有抱怨
foo :: MyType a -> MyOtherType a -> FooT m a

相比之下,当我写了另一种类型的别名

type Bar a b = (Something a, SomethingElse b) => NotAsBar a b

并将其用于负面位置

bar :: Bar a b -> InsertTypeHere

GHC大声喊我错了。

我想我知道发生了什么,但我确信我能从你的解释中更好地掌握,所以我有两个问题:

  • 实际上做什么类型的别名/它们实际意味着什么?
  • 两种情况下都有办法获得简洁吗?

2 个答案:

答案 0 :(得分:12)

类型签名基本上有三个部分:

  1. 变量声明(通常是隐含的)
  2. 变量约束
  3. 类型签名头
  4. 这三个元素基本上是叠加的。类型变量必须在它们可以被使用之前声明,无论是在约束中还是在其他地方,并且类约束的范围超过了类型签名头中的所有用途。

    我们可以重写您的foo类型,以便显式声明变量:

    foo :: forall m a. (MyMonad m) => MyType a -> MyOtherType a -> ListT m a
    

    变量声明由forall关键字引入,并扩展到.。如果您没有明确地介绍它们,GHC会自动将它们放在声明的顶层。接下来是约束,直到=>。其余的是类型签名头。

    看看当我们尝试拼接你的type FooT定义时会发生什么:

    foo :: forall m a. MyType a -> MyOtherType a -> ( (MyMonad m) => ListT m a )
    

    类型变量mfoo的顶层存在,但您的类型别名仅在最终值中添加约束!修复它有两种方法。你可以:

    • 将forall移动到最后,以便m稍后出现
    • 或将类约束移到顶部

    将约束移到顶部看起来像

    foo :: forall m a. MyMonad m => MyType a -> MyOtherType a -> ListT m a
    

    GHC关于启用RankNTypes的建议做了前者(有点,我仍然缺少某些东西),导致:

    foo :: forall a. MyType a -> MyOtherType a -> ( forall m. (MyMonad m) => ListT m a )
    

    这是有效的,因为m没有出现在其他任何地方,而且它在箭头的右侧,所以这两者基本上是相同的。

    bar

    比较
    bar :: (forall a b. (Something a, SomethingElse b) => NotAsBar a b) -> InsertTypeHere
    

    如果类型别名处于否定位置,则较高级别的类型具有不同的含义。现在,bar的第一个参数必须在ab中具有多态性,并具有适当的约束。这与通常的含义不同,bar调用者选择如何实例化那些类型变量。这不是

    最有可能的方法是启用ConstraintKinds扩展,这允许您为约束创建类型别名。

    type BarConstraint a b = (Something a, SomethingElse b)
    
    bar :: BarConstraint a b => NotAsBar a b -> InsertTypeHere
    

    它并不像你希望的那样简洁,但比每次都写出长约束要好得多。

    另一种方法是将您的类型别名更改为GADT,但这可能不会引入其他一些后果。如果您只是希望获得更简洁的代码,我认为ConstraintKinds是最好的选择。

答案 1 :(得分:9)

您可以将类型类约束视为隐式参数 - 即想想

Foo a => b

as

FooDict a -> b

其中FooDict a是类Foo中定义的方法字典。例如,EqDict将是以下记录:

data EqDict a = EqDict { equal :: a -> a -> Bool, notEqual :: a -> a -> Bool }

不同之处在于每种类型的每个字典只能有一个值(适用于MPTC),GHC会为您填写其值。

考虑到这一点,我们可以回到您的签名。

type FooT m a = (MyMonad m) => ListT m a
foo :: MyType a -> MyOtherType a -> FooT m a

扩展为

foo :: MyType a -> MyOtherType a -> (MyMonad m => ListT m a)

使用字典解释

foo :: MyType a -> MyOtherType a -> MyMonadDict m -> ListT m a

相当于重新排序

的参数
foo :: MyMonadDict m -> MyType a -> MyOtherType a -> ListT m a

,相当于字典转换的反转

foo :: (MyMonad m) => MyType a -> MyOtherType a -> ListT m a

这就是你要找的东西。

然而,在你的另一个例子中,事情并没有那么成功。

type Bar a b = (Something a, SomethingElse b) => NotAsBar a b
bar :: Bar a b -> InsertTypeHere

扩展为

bar :: ((Something a, SomethingElse b) => NotAsBar a b) -> InsertTypeHere

这些变量仍在顶层量化(即

bar :: forall a b. ((Something a, SomethingElse b) => NotAsBar a b) -> InsertTypeHere

),因为你在bar的签名中明确提到了它们,但是当我们进行字典转换时

bar :: (SomethingDict a -> SomethingElseDict b -> NotAsBar a b) -> InsertTypeHere

我们可以看到这不等于

bar :: SomethingDict a -> SomethingElseDict b -> NotAsBar a b -> InsertTypeHere

会产生你想要的东西。

很难想出一个现实的例子,其中类型约束被用在与量化点不同的地方 - 我从未在实践中看到它 - 所以这是一个不切实际的例子,只是为了证明那是什么的发生:

sillyEq :: forall a. ((Eq a => Bool) -> Bool) -> a -> a -> Bool
sillyEq f x y = f (x == y)

与我们在未将参数传递给==时尝试使用f时会发生什么情况进行对比:

sillyEq' :: forall a. ((Eq a => Bool) -> Bool) -> a -> a -> Bool
sillyEq' f x y = f (x == y) || x == y

我们得到没有Eq a 错误的实例。

(x == y)中的sillyEqEq获取f字典;它的字典形式是:

sillyEq :: forall a. ((EqDict a -> Bool) -> Bool) -> a -> a -> Bool
sillyEq f x y = f (\eqdict -> equal eqdict x y)
稍微退一步,我认为你在这里苛刻的方式会很痛苦 - 我认为你只是想用一些东西来量化它的上下文,其中它的上下文被定义为“函数签名所在用它“。这个概念没有简单的语义。您应该能够将Bar视为集合上的函数:它将两个集合作为参数并返回另一个集合。我不相信会有你想要达到的功能。

就缩短上下文而言,您可以使用允许您创建约束同义词的ConstraintKinds扩展名,因此至少可以说:

type Bars a = (Something a, SomethingElse a)

获取

bar :: Bars a => Bar a b -> InsertTypeHere

但你想要的仍然是可能的 - 你的名字对我来说不够描述。您可能需要查看Existential QuantificationUniversal Quantification,这是抽象类型变量的两种方法。

故事的道德:记住=>就像->,除了这些参数由编译器自动填充,并确保您尝试使用明确定义的数学定义类型含义。