我的代码经常使用看起来像
的函数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大声喊我错了。
我想我知道发生了什么,但我确信我能从你的解释中更好地掌握,所以我有两个问题:
答案 0 :(得分:12)
类型签名基本上有三个部分:
这三个元素基本上是叠加的。类型变量必须在它们可以被使用之前声明,无论是在约束中还是在其他地方,并且类约束的范围超过了类型签名头中的所有用途。
我们可以重写您的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 )
类型变量m
在foo
的顶层存在,但您的类型别名仅在最终值中添加约束!修复它有两种方法。你可以:
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
的第一个参数必须在a
和b
中具有多态性,并具有适当的约束。这与通常的含义不同,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)
中的sillyEq
从Eq
获取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 Quantification和Universal Quantification,这是抽象类型变量的两种方法。
故事的道德:记住=>
就像->
,除了这些参数由编译器自动填充,并确保您尝试使用明确定义的数学定义类型含义。