为啥 Haskell 没有在函数签名中推断数据类型的类型类?

Posted

技术标签:

【中文标题】为啥 Haskell 没有在函数签名中推断数据类型的类型类?【英文标题】:Why does Haskell stop short of inferring the datatype's typeclasses in the function signatures?为什么 Haskell 没有在函数签名中推断数据类型的类型类? 【发布时间】:2011-01-11 08:05:47 【问题描述】:

首先,这个问题并非 100% 特定于 Haskell,请随意评论类型类、接口和类型的一般设计。

我正在阅读LYAH - creating types and typeclasses以下是我正在寻找更多信息的段落:

Data (Ord k) => Map k v = ...  

但是,这是一个非常强大的约定 在 Haskell 中永远不要添加类型类 数据声明中的约束。为什么? 好吧,因为我们没有得到很多好处, 但我们最终写了更多的课 约束,即使我们不需要 他们。如果我们放置或不放置 Ord k 数据声明中的约束 地图 k v,我们将不得不把 约束成函数 假设地图中的键可以是 订购。但是如果我们不把 数据声明中的约束,我们 不必将 (Ord k) => 放入 函数的类型声明 不在乎钥匙是否可以 订购与否。这样的一个例子 函数是toList,它只需要一个 映射并将其转换为 关联列表。它的类型签名 是 toList :: Map k a -> [(k, a)]。如果 Map k v 在它的 数据声明,toList 的类型 必须是 toList :: (Ord k) => 映射 k a -> [(k, a)],即使 函数不做任何比较 按键顺序。

乍一看,这似乎是合乎逻辑的——但是将类型类附加到类型上没有好处吗?如果类型类是类型的行为,那么为什么要通过使用类型(通过函数)而不是类型本身来定义行为?我假设有一些元编程可以利用它,它肯定是很好的描述性代码文档。相反,这在其他语言中会是一个好主意吗?在方法上指定对象应符合的接口是否理想,这样如果调用者不使用该方法,则对象不必符合接口?此外,为什么 Haskell 不能推断出使用类型 Foo 的函数必须引入类型 Foo 声明中标识的类型类约束?是否有编译指示可以启用此功能?

当我第一次阅读它时,它让人联想到“这是一个 hack(或解决方法)响应”。仔细阅读后,听起来很聪明。在第三次阅读时,将其与 OO 世界进行比较,这听起来又像是一个 hack。

所以我来了。

【问题讨论】:

【参考方案1】:

也许Map k v 不是说明这一点的最佳示例。鉴于Map 的定义,即使有些函数不需要(Ord k) 约束,没有它也无法构造Map

人们经常会发现,即使您将约束设想为原始设计的一个明显方面,也可以在没有特定约束的情况下工作的函数子集非常有用的类型。在这种情况下,将约束从类型声明中去掉会使其更加灵活。

例如,Data.List 包含大量需要 (Eq a) 的函数,但当然列表在没有这种限制的情况下非常有用。

【讨论】:

"没有它就无法构建地图。" -- 这正是我一直觉得 consume 映射的函数上的 Ord 上下文之类的东西是多余的原因,也是 GADT 匹配引入上下文的原因。我有一个Map k v 的事实已经足以证明 k 是有序的。例如,如果我所做的只是从地图中提取内容,我为什么需要一个 Ord 实例?【参考方案2】:

简短的回答是:Haskell 这样做是因为这就是语言规范的编写方式。

长答案涉及引用GHC documentation language extensions section:

可以使用标准 Haskell-98 语法声明的任何数据类型也可以使用 GADT 样式语法声明。选择主要是风格上的,但 GADT 风格的声明在一个重要方面有所不同:它们以不同的方式处理数据构造函数上的类约束。具体来说,如果给构造函数一个类型类上下文,则该上下文可以通过模式匹配获得。例如:

data Set a where
    MkSet :: Eq a => [a] -> Set a

(...)

所有这些行为都与 Haskell 98 对数据类型声明 (Section 4.2.1 of the Haskell 98 Report) 上下文的特殊处理形成鲜明对比。在 Haskell 98 中定义

data Eq a => Set' a = MkSet' [a]

赋予 MkSet' 与上述 MkSet 相同的类型。但是,MkSet' 上的模式匹配不是提供一个 (Eq a) 约束,而是需要一个 (Eq a) 约束! GHC 忠实地实现了这种行为,尽管它很奇怪。但是对于 GADT 样式的声明,GHC 的行为更有用,也更直观。

【讨论】:

【参考方案3】:

在数据声明中避免类型类约束的主要原因是它们完全没有任何作用;事实上,我相信 GHC 将这样的类上下文称为“愚蠢的上下文”。原因是类字典没有携带数据类型的值,所以无论如何你必须将它添加到对值进行操作的每个函数中。

作为一种对操作数据类型的函数“强制”类型类约束的方法,它也没有真正完成任何事情;函数通常应该尽可能多态,那么为什么要强制约束不需要它的东西呢?

此时,您可能认为应该可以更改 ADT 的语义,以便将值携带到字典中。事实上,这似乎就是GADTs 的全部意义所在;例如,你可以这样做:

data Foo a where  Foo :: (Eq a) => a -> Foo a 
eqfoo :: Foo t -> Foo t -> Bool
eqfoo (Foo a) (Foo b) = a == b

注意 eqfoo 的类型不需要 Eq 约束,因为它是由 Foo 数据类型本身“携带”的。

【讨论】:

我对 Haskell 还很陌生,简要解释一下 GADT 是什么会有所帮助(或链接)。 我对该部分进行了一些修改,并添加了指向 GADT 的 GHC 文档的链接。如果其他人想更好地解释什么是 GADT,那将是受欢迎的!【参考方案4】:

我想指出,如果您担心可以构造一个对象,该对象的操作需要约束但创建时不需要约束,例如 mkFoo,您总是可以人为地将约束放在 mkFoo 函数上以强制执行使用代码的人对类型类的使用。这个想法还扩展到对 Foo 进行操作的非 mkFoo 类型函数。然后在定义模块时,不要导出任何不强制约束的东西。

虽然我不得不承认,但我认为这没有任何用处。

【讨论】:

以上是关于为啥 Haskell 没有在函数签名中推断数据类型的类型类?的主要内容,如果未能解决你的问题,请参考以下文章

为啥我的 Haskell 函数参数必须是 Bool 类型?

我可以验证给定的函数类型签名是不是具有潜在的实现吗?

为啥 C# 无法从非泛型静态方法的签名推断泛型类型参数类型?

对 Haskell 类型推断感到困惑

Haskell 对啥不满意?它在抱怨我的类型签名

为啥类型推断算法会因为“Fun.flip Option.bind”而混淆?