为什么要使用类型类而不是仅使用模式匹配?

7
这是一个哲学问题,但我希望能够通过官方文档或“神的话语”(即SPJ)得到答案。Haskell委员会是否有特定原因选择要求使用类型类形式的显式接口,而不是基于模式匹配的更统一的解决方案?以Eq为例:
class Eq a where
    (==), (/=) :: a -> a -> Bool
    x == y = not $ x /= y
    x /= y = not $ x == y

instance Eq Int where
    (==) = internalIntEq

为什么我们不能像这样做(请忍受伪Haskell):
(==), (/=) :: a -> a -> Bool
default x == y = not $ x /= y             -- 1
default x /= y = not $ x == y

(Int a) == (Int b) = a `internalIntEq` b  -- 2

也就是说,如果Haskell允许对普通数据类型进行模式匹配,则:
  • 程序员可以创建特定的类,即instance将是隐式的 (2)

  • 类型仍然可以被静态地推断和匹配 (SupportsEqualsEquals a => ...)

  • 默认实现将会“自动”生成

  • 类可以轻松扩展而不会破坏任何东西

需要有一种方法来指定一个默认模式(1),尽管在其他模式声明之前,但总是最后匹配。这些假设功能中是否有任何与Haskell固有特性相冲突的内容?推断类型会变得困难或不可能吗?看起来这是一个非常强大的功能,很好地与Haskell的其余部分结合,因此我认为我们没有采用这种方式的充分理由。这种特定的多态机制是否过于特定了?


我对此不是很熟悉,但我认为一个主要问题是你会失去参数性,从而失去了自由定理:你仅通过函数的类型签名就知道 id :: a -> a 或者 fst :: (a,b) -> a 的作用,因为它们只有参数多态性。如果你有某种类型情况机制,那么你就会失去参数性。例如,a -> a -> Bool 在 Haskell 中只有两个(总)实现方式(const $ const Trueconst $ const False);如果你有类型情况,那么你就会失去这个保证。 - Antal Spector-Zabusky
@AntalS-Z:只是确认我理解你的意思。你是说我可以声明 id :: a -> a,然后定义 id (Int i) = i + 1 吗?因为这不是事实:后面的定义将是类型 SupportsPlus a => a -> a,与之不兼容。 - Jon Purdy
5
根据你的问题,你提供了(==)的类型签名为a -> a -> Bool。如果你必须指定一个“类型类” SupportsEquals,那么看起来你只是在交换语法 - 我不明白为什么它会不同。我认为它不能表达多参数类型类、函数依赖或关联类型,尽管这些当然不在最初的计划中。实际上,它甚至能否表达更高维度的类型类?你如何知道return :: a -> m a应该针对于m而不是a进行特别处理? - Antal Spector-Zabusky
1
@JonPurdy 它是如何获得那种类型的?i 明显是一个 Int,它是如何被推导为 SupportsPlus a => a 的?同样的泛化规则,无论是什么,是否适用于所有定义或仅适用于使用 typecase 符号的定义?这是否意味着,即使没有全局的 id :: a -> a 签名,id (Int i) = i + 1 的类型也是 SupportsPlus a => a -> a"?如果是这样,像 id 3.7 这样的表达式的语义是什么? - Doug McClean
2
我强烈反对关闭投票。这是一个开放性的、主观的问题,没错。但我认为探索一门语言为什么以某种方式设计是一项很棒的智力锻炼。 - gatoatigrado
显示剩余5条评论
2个回答

12

这种特殊的多态机制是否过于特殊呢?

这个问题简直就是要和Philip Wadler和Steve Blott的1988年论文如何让特殊多态性变得不那么特殊联系起来,他们提出了类型类的概念。Wadler在这方面可能是“上帝之言”。

我看到了一些问题,关于所提出的“对任何Haskell数据类型进行模式匹配”的技术。

模式匹配技术不足以定义多态常量,例如mempty :: Monoid a => a

模式匹配技术仍然回归到类型类,只不过方式更糟。类型类分类类型(想想看)。但是模式匹配技术使它变得相当模糊。你应该如何指定函数foobar属于“同一”类呢?如果您必须为每个单独的多态函数添加一个新的类型类约束,则类型类约束将变得无法读取

模式匹配技术引入了新的语法到Haskell中,使语言规范变得更加复杂。关键字“default”看起来还不错,但是对于“类型上的模式匹配”是新的且令人困惑的。
“普通数据类型的模式匹配”打败了无点风格。我们有(Int a) == (Int b) = intEq a b而不是(==) = intEq;这种人为的模式匹配“阻止了η-约简”。
最后,它完全改变了我们对类型签名的理解。 a -> a -> Foo目前是一个保证,即输入不能被检查。除了两个输入是相同类型之外,不能假设任何关于a输入的内容。 [a] -> [a]再次意味着列表元素在任何有意义的方式下都“无法检查”,给你Theorems for Free(另一篇Wadler论文)。

也许有解决这些问题的方法,但我的总体印象是,类型类已经以一种优雅的方式解决了这个问题,而建议的模式匹配技术没有任何好处,反而会引起几个问题。


1
关于你最后提到的一点,我认为他的建议并不是完全改变多态性,而只是为每个使用的函数添加一堆类型约束。例如 a -> a -> Foo 仍然可以保证输入不能被检查 -- 如果使用了隐式类型类,那么它将成为签名的一部分。 - gatoatigrado
非常感谢您的见解和论文。不过,我对您的一些观点持有不同意见。类型类约束不会变得难以理解,因为它们甚至不需要可见性——您只需说“(+) not defined in (foo::Foo + bar::Bar)”。关于类型签名的观点很重要。您知道id :: a -> a仍然成立,因为涉及任何其他函数的定义都将具有不兼容的签名。此外,关于 eta-reduction,您正在编写一个应用程序(而不是连接)语言,因此并非所有内容都可以无点表示。 - Jon Purdy
@gatoatigrado:是的,这正是我想表达的。它不会违反我们已经拥有的任何定理。至少我认为是这样的。 - Jon Purdy

4

我不知道上帝的话语,但这里有一些关于IT技术的论点。

在同一个模块中定义函数不再是唯一的方法。现在你可以这样写:

(==) = internalIntEq
(==) = internalFloatEq

这会使代码不易读。有一个名为“TypeBasedNameResolution”的提案,它做了类似的事情,但重要的事实是这种类型分支仅针对来自不同模块的(==)
将编译器添加标识符是不好的实践。在您的情况下,您自动创建了类型类SupportsEqualsEquals。新用户可能会问:“这是从哪里来的?”,并且没有相应的源定义。
跳过编写实例签名并不能给您带来您想象中的那么多。您可以通过例如在ghci中使用:t internalIntEq获得必要的参数。我想这可能更方便,但我宁愿拥有一个工具,我可以问“当==internalIntEq时,Eq的实例类型是什么”。
更高级的类特性不清楚。您将关联类型和功能依赖项放在哪里?这对我来说真的很重要!
您的默认值使模块化编译变得更加困难。您不会免费获得可扩展的类。请考虑,
f :: Supports[==] a => a -> a -> Bool
f = (/=)

据我理解,这将编译成以下内容:
f :: Instance (Supports[==]) a -> a -> a -> Bool
f eq_inst x y = not (x eq_inst.== y)

现在,如果我为特定类型 a_0 提供一个新的 /= 实例,并将一些 x :: a_0 输入到 f 中,则
f x x = not (x == x)
-- computation you really want: f x x = x /= x, using the new /= instance for a_0

你可能会问,“什么时候会有人如此愚蠢,将 f 限制在 Supports[==] 而不是 Supports[/=] 中?”但是,上下文可以来自更高级的函数签名,例如高阶函数等等。
希望这能帮到你。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接