理解具有类约束的等级2类型别名

时间:2021-02-23 17:02:26

I have code that frequently uses functions that look like

我的代码经常使用类似的函数

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

To try to shorten this, I wrote the following type alias:

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

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

GHC asked me to turn on Rank2Types (or RankNTypes), but didn't complain when I used the alias to shorten my code to

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

foo :: MyType a -> MyOtherType a -> FooT m a

By contrast, when I wrote another type alias

相反,当我写另一个类型别名时

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

and used it in a negative position

用在否定的位置

bar :: Bar a b -> InsertTypeHere

GHC loudly yelled at me for being wrong.

GHC大声骂我错了。

I think I have an idea of what's going on, but I'm sure I could get a better grasp from your explanations, so I have two questions:

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

  • What are the type aliases actually doing/what do they actually mean?
  • 什么类型的别名实际上在做/它们实际上是什么意思?
  • Is there a way to get the terseness in both cases?
  • 在这两种情况下,是否有一种方法可以做到简洁?

2 个解决方案

#1


12  

There are essentially three parts to a type signature:

类型签名有三部分:

  1. variable declarations (these are usually implicit)
  2. 变量声明(通常是隐式的)
  3. variable constraints
  4. 变量的约束
  5. the type signature head
  6. 签名的头类型

These three elements essentially stack. Type variables must be declared before they can be used, either in constraints or elsewhere, and a class constraint scopes over all uses within the type signature head.

这三个元素本质上是堆栈。在可以使用类型变量之前,必须声明类型变量(无论是在约束中还是在其他地方),并且在类型签名头中的所有用途上都要声明类约束作用域。

We can rewrite your foo type so the variables are explicitly declared:

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

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

The variable declarations are introduced by the forall keyword, and extend to the .. If you don't explicitly introduce them, GHC will automatically scope them at the top level of the declaration. Constraints come next, up to the =>. The rest is the type signature head.

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

Look at what happens when we try to splice in your type FooT definition:

看看当我们试图在你的脚类型定义中拼接时会发生什么:

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

The type variable m is brought into existence at the top level of foo, but your type alias adds a constraint only within the final value! There are two approaches to fixing it. You can either:

类型变量m出现在foo的顶层,但是您的类型别名只在最终值中添加了一个约束!有两种方法可以修复它。你可以:

  • move the forall to the end, so m comes into existence later
  • 把forall移动到最后,m就会在后面出现
  • or move the class constraint to the top
  • 或者将类约束移动到顶部

Moving the constraint to the top looks like

将约束移动到顶部看起来是这样的

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

GHC's suggestion of enabling RankNTypes does the former (sort of, there's something I'm still missing), resulting in:

GHC关于启用RankNTypes的建议实现了前者(某种程度上,我还缺少一些东西),导致:

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

This works because m doesn't appear anywhere else, and it's right of the arrow, so these two mean essentially the same thing.

这是因为m在其他地方没有出现,它是箭头的右边,所以这两个本质上是一样的。

Compare to bar

与酒吧

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

With the type alias in a negative position, a higher-rank type has a different meaning. Now the first argument to bar must be polymorphic in a and b, with appropriate constraints. This is different from the usual meaning, where bars caller chooses how to instantiate those type variables. It's not

当类型别名位于负值时,级别较高的类型有不同的含义。现在,bar的第一个参数必须是a和b中的多态性,并有适当的约束。这与通常的含义不同,bar调用者选择如何实例化这些类型变量。这不是

In all likelihood, the best approach is to enable the ConstraintKinds extension, which allows you to create type aliases for constraints.

在所有可能性中,最好的方法是启用constrainttypes扩展,这允许您为约束创建类型别名。

type BarConstraint a b = (Something a, SomethingElse b)

bar :: BarConstraint a b => NotAsBar a b -> InsertTypeHere

It's not quite as terse as what you hoped for, but much better than writing out long constraints every time.

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

An alternative would be to change your type alias into a GADT, but that has several other consequences you may not want to bring in. If you're simply hoping to get more terse code, I think ConstraintKinds is the best option.

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

#2


9  

You can think of typeclass constraints essentially as implicit parameters -- i.e. think of

您可以将类型类约束看作隐式参数,也就是考虑。

Foo a => b

as

作为

FooDict a -> b

where FooDict a is a dictionary of methods defined in the class Foo. For example, EqDict would be the following record:

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

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

The differences are that there can only be one value of each dictionary at each type (generalize appropriately for MPTCs), and GHC fills in its value for you.

不同之处在于,每种类型的每个字典只能有一个值(对MPTCs进行适当的概括),GHC为您填充它的值。

With this in mind, we can come back to your signatures.

记住这一点,我们可以回到你的签名。

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

expands to

扩大到

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

using the dictionary interpretation

使用字典的解释

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

which is equivalent by reordering of arguments to

哪个是等价于重排序的参数

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

which is equivalent by the inverse of the dictionary transformation to

它等价于字典变换的逆

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

which is what you were looking for.

这就是你要找的。

However, things do not work out that way in your other example.

然而,在您的另一个示例中,事情不是这样的。

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

expands to

扩大到

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

These variables are still quantified at the top level (i.e.

这些变量仍然在顶层被量化。

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

), since you mentioned them explicitly in bar's signature, but when we do the dictionary transformation

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

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

we can see that this is not equivalent to

我们可以看到,这不是等价的

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

which would give rise to what you want.

这就会产生你想要的东西。

It's pretty tough to come up with realistic examples in which a typeclass constraint is used at a different place than its point of quantification -- I have never seen it in practice -- so here's an unrealistic one just to show that that's what's happening:

很难想出一个现实的例子,在这个例子中,一个类型的约束被用在一个不同的地方,而不是它的量化点——我从来没有在实践中看到过它——所以这里有一个不现实的例子,只是为了表明这是正在发生的事情:

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

Contrast to what happens if we use try to use == when we are not passing an argument to f:

与我们不向f传递参数时使用==的情况形成对比:

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

we get a No instance for Eq a error.

我们得到一个无实例的Eq a错误。

The (x == y) in sillyEq gets its Eq dict from f; its dictionary form is:

(x == y)在西约q得到f;词典的形式是:

sillyEq :: forall a. ((EqDict a -> Bool) -> Bool) -> a -> a -> Bool
sillyEq f x y = f (\eqdict -> equal eqdict x y)

Stepping back a bit, I think the way you are tersifying here is going to be painful -- I think you're wanting the mere use of something to quantify its context, where its context is defined as the "function signature where it is used". That notion has no simple semantics. You should be able to think of Bar as function on sets: it takes as arguments two sets and returns another. I don't believe there will be such a function for the use you are trying to achieve.

退一步说,我认为您在这里简化的方式将是痛苦的——我认为您希望仅仅使用一些东西来量化它的上下文,其中的上下文被定义为“使用它的函数签名”。这个概念没有简单的语义。您应该能够将Bar视为集合上的函数:它接受两个集合作为参数并返回另一个集合。我不相信会有这样一个功能,用于你想要达到的用途。

As far as shortening contexts, you may be able to make use of the ConstraintKinds extension which allows you to make constraint synonyms, so at least you could say:

至于起缩上下文,您可以使用constrainttypes扩展,它允许您将约束同义词设置为同义词,因此,至少您可以说:

type Bars a = (Something a, SomethingElse a)

to get

得到

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

But what you want still may be possible -- your names are not descriptive enough for me to tell. You may want to look into Existential Quantification and Universal Quantification, which are two ways of abstracting over type variables.

但你想要的还是有可能的——你的名字还不足以让我说出。您可能想研究存在量化和通用量化,它们是对类型变量进行抽象的两种方法。

Moral of the story: remember that => is just like -> except that those arguments are filled in automatically by the compiler, and make sure that you are trying to define types with well-defined mathematical meanings.

故事的寓意:记住=>就像->,除了那些参数是由编译器自动填充的,并且确保您正在尝试定义具有定义良好的数学意义的类型。

#1


12  

There are essentially three parts to a type signature:

类型签名有三部分:

  1. variable declarations (these are usually implicit)
  2. 变量声明(通常是隐式的)
  3. variable constraints
  4. 变量的约束
  5. the type signature head
  6. 签名的头类型

These three elements essentially stack. Type variables must be declared before they can be used, either in constraints or elsewhere, and a class constraint scopes over all uses within the type signature head.

这三个元素本质上是堆栈。在可以使用类型变量之前,必须声明类型变量(无论是在约束中还是在其他地方),并且在类型签名头中的所有用途上都要声明类约束作用域。

We can rewrite your foo type so the variables are explicitly declared:

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

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

The variable declarations are introduced by the forall keyword, and extend to the .. If you don't explicitly introduce them, GHC will automatically scope them at the top level of the declaration. Constraints come next, up to the =>. The rest is the type signature head.

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

Look at what happens when we try to splice in your type FooT definition:

看看当我们试图在你的脚类型定义中拼接时会发生什么:

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

The type variable m is brought into existence at the top level of foo, but your type alias adds a constraint only within the final value! There are two approaches to fixing it. You can either:

类型变量m出现在foo的顶层,但是您的类型别名只在最终值中添加了一个约束!有两种方法可以修复它。你可以:

  • move the forall to the end, so m comes into existence later
  • 把forall移动到最后,m就会在后面出现
  • or move the class constraint to the top
  • 或者将类约束移动到顶部

Moving the constraint to the top looks like

将约束移动到顶部看起来是这样的

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

GHC's suggestion of enabling RankNTypes does the former (sort of, there's something I'm still missing), resulting in:

GHC关于启用RankNTypes的建议实现了前者(某种程度上,我还缺少一些东西),导致:

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

This works because m doesn't appear anywhere else, and it's right of the arrow, so these two mean essentially the same thing.

这是因为m在其他地方没有出现,它是箭头的右边,所以这两个本质上是一样的。

Compare to bar

与酒吧

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

With the type alias in a negative position, a higher-rank type has a different meaning. Now the first argument to bar must be polymorphic in a and b, with appropriate constraints. This is different from the usual meaning, where bars caller chooses how to instantiate those type variables. It's not

当类型别名位于负值时,级别较高的类型有不同的含义。现在,bar的第一个参数必须是a和b中的多态性,并有适当的约束。这与通常的含义不同,bar调用者选择如何实例化这些类型变量。这不是

In all likelihood, the best approach is to enable the ConstraintKinds extension, which allows you to create type aliases for constraints.

在所有可能性中,最好的方法是启用constrainttypes扩展,这允许您为约束创建类型别名。

type BarConstraint a b = (Something a, SomethingElse b)

bar :: BarConstraint a b => NotAsBar a b -> InsertTypeHere

It's not quite as terse as what you hoped for, but much better than writing out long constraints every time.

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

An alternative would be to change your type alias into a GADT, but that has several other consequences you may not want to bring in. If you're simply hoping to get more terse code, I think ConstraintKinds is the best option.

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

#2


9  

You can think of typeclass constraints essentially as implicit parameters -- i.e. think of

您可以将类型类约束看作隐式参数,也就是考虑。

Foo a => b

as

作为

FooDict a -> b

where FooDict a is a dictionary of methods defined in the class Foo. For example, EqDict would be the following record:

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

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

The differences are that there can only be one value of each dictionary at each type (generalize appropriately for MPTCs), and GHC fills in its value for you.

不同之处在于,每种类型的每个字典只能有一个值(对MPTCs进行适当的概括),GHC为您填充它的值。

With this in mind, we can come back to your signatures.

记住这一点,我们可以回到你的签名。

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

expands to

扩大到

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

using the dictionary interpretation

使用字典的解释

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

which is equivalent by reordering of arguments to

哪个是等价于重排序的参数

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

which is equivalent by the inverse of the dictionary transformation to

它等价于字典变换的逆

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

which is what you were looking for.

这就是你要找的。

However, things do not work out that way in your other example.

然而,在您的另一个示例中,事情不是这样的。

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

expands to

扩大到

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

These variables are still quantified at the top level (i.e.

这些变量仍然在顶层被量化。

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

), since you mentioned them explicitly in bar's signature, but when we do the dictionary transformation

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

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

we can see that this is not equivalent to

我们可以看到,这不是等价的

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

which would give rise to what you want.

这就会产生你想要的东西。

It's pretty tough to come up with realistic examples in which a typeclass constraint is used at a different place than its point of quantification -- I have never seen it in practice -- so here's an unrealistic one just to show that that's what's happening:

很难想出一个现实的例子,在这个例子中,一个类型的约束被用在一个不同的地方,而不是它的量化点——我从来没有在实践中看到过它——所以这里有一个不现实的例子,只是为了表明这是正在发生的事情:

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

Contrast to what happens if we use try to use == when we are not passing an argument to f:

与我们不向f传递参数时使用==的情况形成对比:

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

we get a No instance for Eq a error.

我们得到一个无实例的Eq a错误。

The (x == y) in sillyEq gets its Eq dict from f; its dictionary form is:

(x == y)在西约q得到f;词典的形式是:

sillyEq :: forall a. ((EqDict a -> Bool) -> Bool) -> a -> a -> Bool
sillyEq f x y = f (\eqdict -> equal eqdict x y)

Stepping back a bit, I think the way you are tersifying here is going to be painful -- I think you're wanting the mere use of something to quantify its context, where its context is defined as the "function signature where it is used". That notion has no simple semantics. You should be able to think of Bar as function on sets: it takes as arguments two sets and returns another. I don't believe there will be such a function for the use you are trying to achieve.

退一步说,我认为您在这里简化的方式将是痛苦的——我认为您希望仅仅使用一些东西来量化它的上下文,其中的上下文被定义为“使用它的函数签名”。这个概念没有简单的语义。您应该能够将Bar视为集合上的函数:它接受两个集合作为参数并返回另一个集合。我不相信会有这样一个功能,用于你想要达到的用途。

As far as shortening contexts, you may be able to make use of the ConstraintKinds extension which allows you to make constraint synonyms, so at least you could say:

至于起缩上下文,您可以使用constrainttypes扩展,它允许您将约束同义词设置为同义词,因此,至少您可以说:

type Bars a = (Something a, SomethingElse a)

to get

得到

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

But what you want still may be possible -- your names are not descriptive enough for me to tell. You may want to look into Existential Quantification and Universal Quantification, which are two ways of abstracting over type variables.

但你想要的还是有可能的——你的名字还不足以让我说出。您可能想研究存在量化和通用量化,它们是对类型变量进行抽象的两种方法。

Moral of the story: remember that => is just like -> except that those arguments are filled in automatically by the compiler, and make sure that you are trying to define types with well-defined mathematical meanings.

故事的寓意:记住=>就像->,除了那些参数是由编译器自动填充的,并且确保您正在尝试定义具有定义良好的数学意义的类型。