Haskell函数签名中具有多个类型变量的类型约束

Type constraints with multiple type variables in Haskell function signature

提问人:NickS1 提问时间:4/29/2023 最后编辑:NickS1 更新时间:5/1/2023 访问量:299

问:

我一直在文档和 wiki 上搜索函数签名中的 Haskell 类型约束。不幸的是,我无法找到一个满意的答案。

目前,我认为自己是初学者,所以如果您看到滥用技术术语或缺乏有关此主题的高级知识,我想询问您的理解。

第一种情况

考虑以下函数签名给出的一个真实示例:

count :: (Eq e, Num n) => e -> [e] -> n -> n

此函数具有类型约束:和,每个约束分别只有一个变量:和。我也可以提供一个只有一个约束的更简单的签名(例如)。EqNumenfoo :: Eq a => a -> Bool

但是以下情况呢?

第二种情况

考虑假设的例子。我想到他们试图表示属于同一类型约束的多个变量。

-- As far as I tested, the examples below are incorrect
-- and do not reflect proper Haskell syntax

foo :: (Num a b c) => use the variables here

or

foo :: Num a, b, c => use the variables here

我们可以在同一约束中指定多个变量,就像我上面尝试的那样吗?我是否必须单独指定它们?

foo :: Eq a, Eq b ... => use the variables here

有没有使用相同类型的多个类型变量的情况,这会带来什么影响?


编辑 - 2023 年 5 月 1 日:我接受了 Ben 的回答,因为我相信它彻底而丰富地解释了我的问题。我真的发现它澄清了,非常有建设性,因为他还补充了约瑟夫的答案,这些答案与上下文相关。

Haskell 约束类型 变量函数 签名

评论


答:

3赞 Joseph Sible-Reinstate Monica 4/29/2023 #1

您可以编写一个类型同义词,用于约束许多类型,而不必每次都重复该约束,如下所示:

import Data.Kind (Constraint)

type Three c t1 t2 t3 = (c t1, c t2, c t3) :: Constraint

allEqual :: Three Eq a b c => a -> a -> b -> b -> c -> c -> Bool
allEqual x1 x2 y1 y2 z1 z2 = x1 == x2 && y1 == y2 && z1 == z2

评论

0赞 NickS1 4/29/2023
很好的答案!非常感谢!有没有办法使用原始的 Haskell 语法来编写类似的东西?
0赞 Joseph Sible-Reinstate Monica 4/29/2023
@NickS1 你不认为我的答案的哪一部分是“原始的Haskell语法”?
0赞 Joseph Sible-Reinstate Monica 4/29/2023
@NickS1 是标准 Haskell 的一部分,也是标准 C 的一部分(请注意,有些模块以它开头,但事实并非如此。Data.Kindstdio.hData.
3赞 Iceland_jack 4/29/2023
如果要部分应用 Three,则需要约束同义词编码:class (c t1, c t2, c t3) => Three c t1 t2 t3 instance (c t1, c t2, c t3) => Three c t1 t2 t3
2赞 Ben 5/1/2023
@NickS1 导入模块“原始 Haskell”的一部分,也是重要的一部分。在大多数情况下,只有在不导入任何内容的情况下才能完成基本的教程练习;要编写任何实用的代码,您需要习惯于导入。的确,安装可选库(以及如何管理依赖项)是初学者以后应该学习的主题,但是在没有任何依赖项管理的情况下,任何 GHC 安装都会附带相当多的模块。您可以而且应该使用它们,不要人为地将自己限制在基本的 .Prelude
4赞 Iceland_jack 4/29/2023 #2

您还可以定义一个类型系列来约束列表的每个元素:

{-# Language DataKinds, TypeFamilies #-}

import Data.Kind (Constraint)

type
  AllCls :: (k -> Constraint) -> ([k] -> Constraint)
type family
  AllCls cls as where
  AllCls cls '[]    = ()
  AllCls cls (a:as) = (cls a, AllCls cls as)

allEqual :: AllCls Eq [a, b, c] => a -> a -> b -> b -> c -> c -> Bool
allEqual x1 x2 y1 y2 z1 z2 = and
  [ x1 == x2
  , y1 == y2
  , z1 == z2
  ]

在AllCls方面可以实现三个。

type Three :: (k -> Constraint) -> (k -> k -> k -> Constraint)
type Three cls a b c = AllCls cls '[a, b, c]
2赞 Ben 5/1/2023 #3

约束的语法是,在箭头的左边,你必须有一个单一的东西。=>Constraint

()是一个(始终满足且不提供任何功能的空的)。任何单一类型的类约束显然都是 .一对约束也是一个 ,必须写成 。同样是三倍、四倍等。如果需要多个约束,请将它们放在用逗号分隔的列表中,并用括号括起来。ConstraintConstraintConstraint(c1, c2)

基本类型类名称(如 、 等)是生产者。它们有种类,即你把它们应用到一个类型(最常见的是变量)上,使一个.所以我可以应用类型变量来做一个喜欢.EqNumConstraintType -> ConstraintConstraintNumaConstraintNum a

但没有意义的;它需要是可以应用于三种类型的东西来制作.这样的东西会有一种,类似的东西可以存在,所以我们不能自动应用于所有 3 个变量,而不会干扰将多参数类型类应用于多个变量的语法。Num a b cNumConstraintType -> Type -> Type -> ConstraintNum a b cNum

如果你希望所有的 , 和 都有约束,那么 Haskell 的“内置”语法就是 。也许有点重复,但是由于类型类约束声明了用于处理类型值的接口,因此需要明确此信息。Haskell函数具有大量类型变量的情况也不常见,因此无论如何,在签名中重复多个类型类名是一个非常罕见的问题。abcNum(Num a, Num b, Num c)abc

尽管如此,如果您想使用更高级的 Haskell,还是有一些方法可以解决这个问题。在 Haskell1 中,s(以及产生它们的东西,如 、 等)是完全普通的类型级实体,因此任何可用于操作类型的特征都可以用来操作 s。因此,即使没有将单个约束生产者(如)应用于多个类型的内置语法,您也可以一些事情来完成这项工作。其他答案显示了几种方法。如果你在Haskell学习的早期阶段,我可能会建议你现在坚持手动写出所有的约束,但这取决于你。ConstraintNumEqConstraintNum


1 至少,它们与语言扩展有关,但这被广泛接受,现在在支持该语言的编译器上默认启用。ConstraintKindsGHC2021


有没有使用相同类型的多个类型变量的情况,这会带来什么影响?

您是在谈论使用具有类型类约束的多个类型变量吗?说像 ?是的,有时会这样做。pairEqual :: (Eq a, Eq b) => (a, b) -> (a, b) -> Bool

每个变量(和约束)的含义都一如既往。调用方可以选择成为他们喜欢的任何类型(具有实例),也可以选择成为他们喜欢的任何类型(具有实例)。的代码可以使用类的接口来获取 type 和 的值(即它可以测试值的相等性,也可以测试值的相等性)。pairEqualaEqbEqpairEqualEqabab

但重要的是,当使用多个类型变量时,这些选择和接口是完全相互独立的。的调用者可以完全独立地选择类型;它选择 be 和 to be(例如,通过调用它 )。 并且不必彼此有任何关系。pairEqualabaCharbMaybe BoolpairEqual ('a', Just False) ('a', Nothing)ab

并且代码实现可以调用两个值来测试一个类型的值是否等于另一个类型的值,它可以对值做同样的事情,但它没有能力测试一个值是否等于一个值。它有权访问的两个接口彼此独立。pairEqual==aaababEq

因此,这与我们有 .这里只有一个类型变量供调用方选择;两对的两个元素必须是相同的类型。这为代码实现提供了更多的自由度;它知道两个对的两个元素都属于一个类型,并且都与它所拥有的接口一起工作。因此,如果它愿意,它可以测试第一对的第一个元素是否等于第二对的第二个元素。我们之前的 two-type-variable 无法做到这一点。但它给人们打电话的自由更少;now 无效,因为没有类型可以为允许值和 的单一类型变量选择。pairEqual :: Eq a => (a, a) -> (a, a) -> BoolpairEqualEqpairEqualpairEqualpairEqual ('a', Just False) ('a', Nothing)a'a'Just False

因此,使用更多或更少的类型变量实际上取决于您要执行的操作。使用更多可以“更好”,因为它为函数的调用者提供了更多的自由(即它使函数在更多情况下有用)。对使用更多独立类型的函数代码的限制也有助于确保不会意外地编写错误的代码。但是,对于完全独立类型的值,通常没有太多事情可以做,因此您不能只为每个输入值提供自己的类型变量。

评论

1赞 NickS1 5/1/2023
你好!非常感谢您的贡献,您的解释既丰富又清晰,我真的很感激!