【问题标题】:In Haskell, why is there a typeclass hierarchy/inheritance?在 Haskell 中,为什么有类型类层次结构/继承?
【发布时间】:2020-08-30 18:35:18
【问题描述】:

为了澄清我的问题,让我以或多或少等效的方式重新表述它:

为什么在 Haskell 中有超类/类继承的概念? 导致这种设计选择的历史原因是什么? 例如,拥有一个没有类层次结构、只有相互独立的类型类的基础库,为什么会如此糟糕?

在这里,我将揭露一些让我想问这个问题的随机想法。我目前的直觉可能是不准确的,因为它们是基于我目前对 Haskell 的理解,这并不完美,但它们在这里......

对我来说,为什么在 Haskell 中存在类型类继承并不明显。我觉得这有点奇怪,因为它在概念上造成了不对称。 通常在数学中,可以从不同的角度定义概念,我不一定要支持应该如何定义它们的顺序。好吧,人们应该按照某种顺序证明事物,但是一旦定理和结构出现,我宁愿将它们视为可用的独立工具。

此外,我在类继承中看到的一件可能不太好的事情是:我认为一个类实例会默默地选择一个相应的超类实例,它可能被实现为该类型最自然的实例。让我们考虑一个被视为 Functor 子类的 Monad。也许有不止一种方法可以在某种类型上定义 Functor,而这种类型也恰好是 Monad。但是说一个 Monad 是一个 Functor 隐含地为那个 Monad 选择了一个特定的 Functor。有一天,你可能会忘记实际上你想要一些其他的 Functor。 也许这个例子不是最合适的,但我觉得这种情况可能会泛化,如果你的班级是许多孩子的孩子,可能会很危险。当前的 Haskell 继承听起来像是隐式地对父母进行了默认选择。

如果您的设计没有层次结构,我觉得您总是必须明确所有所需的属性,这可能意味着风险更小、更清晰、更对称。到目前为止,我所看到的是这种设计的成本将是:对于从一组概念到另一组概念的每次有意义的转换,在实例定义和新类型包装器中编写更多的约束。我不确定,但也许这是可以接受的。不幸的是,我认为新类型的 Haskell 自动派生机制不能很好地工作,我希望该语言在新类型包装/展开方面更智能,并且需要更少的冗长。 我不确定,但现在我想一想,也许新类型包装器的替代方案可能是包含特定实例变体的模块的特定导入。

我在写这篇文章时想到的另一种选择是,也许可以削弱class (P x) => C x 的含义,而不是要求C 的实例选择P 的实例,我们可以只粗略地说,例如,C 类也包含P 的方法,但没有自动选择P 的实例,与P 不存在其他关系。所以我们可以保留某种较弱的层次结构,可能更灵活。

感谢您对该主题的一些澄清,和/或纠正我可能的误解。

【问题讨论】:

  • 对此我无法给出明确的答案,因此我将发表评论。两点。第一,在许多情况下具有层次结构是有意义的:Monoid,无论是在数学上还是在 Haskell 中,都必须是 Semigroup(你的 Functor 示例也是一个很好的例子)。二、多类型类定义的问题独立于继承而存在。例如,您可以使列表的Applicative 实例使用普通的apZipList pure。不幸的是,我们为类定义的定律在标准的 haskell 中不容易证明。然而,违法是继承与否的问题。
  • 我不确定我是否理解您声称的某些内容。 Haskell 中的 AFAIU 类型类层次结构表示数学中的约束。 Applicative Functor 必然是数学中的 Functor。所以约束Functor f => Applicative f 正好代表:“只有函子也可以是应用函子”。我也很好奇你说“c 选择了 p 的 an 实例”。没有这样的“选择”。它的意思是“要使 x 成为 C,x 首先需要成为 P”。不涉及选择。
  • But saying that a Monad is a Functor implicitly makes the choice of one particular Functor for that Monad. 但这只是对事物现状的一种表达。一旦你有了 Monad,就会有一个且只有一个合理的 Functor 实例,由 fmap f x = x >>= (return . f) 给出
  • 我相信这主要是为了方便。在很久以前,我们将Monad 作为Functor 的独立类(应用程序甚至不存在),因此如果我们想重用需要函子的函数,我们必须经常编写f :: (Monad m, Functor m) => ...。这感觉很愚蠢,因为 fmap f x = x >>= return . f 将任何 monad 变成了函子。
  • 我明白,对于像 monad 这样简单的东西,实际上只有一种方法可以从中定义函子,将 monad 视为比函子。但我的观点是,对于更复杂的结构,可能没有明确的自然层次结构,可能有很多不同的路径,我不知道。所以强制一个层次结构会在如何建模某些问题时产生很多不对称和偏见.

标签: haskell typeclass


【解决方案1】:

也许你已经厌倦了听到我的消息,但是这里……

我认为超类是作为类型类的一个相对次要和不重要的特性引入的。在 Wadler and Blott, 1988 中,它们在第 6 节中简要讨论,其中给出了示例 class Eq a => Num a。在那里,唯一提供的理由是,在函数类型中写入(Eq a, Num a) => ... 是很烦人的,而应该“明显”可以添加、乘法和求反的数据类型也应该可以测试相等性。超类关系允许“方便的缩写”。

(这个特性的不重要性被这个例子如此糟糕的事实所强调。现代 Haskell 没有class Eq a => Num a,因为所有Nums 也是Eqs 的逻辑理由太弱了。 class Eq a => Ord a 的例子会更有说服力。)

因此,在没有任何超类的情况下实现的基本库看起来或多或少是相同的。库和用户代码中的函数类型签名只会有更多逻辑上多余的约束,而不是回答这个问题,我会回答一个关于为什么的初学者问题:

leq :: (Ord a) => a -> a -> Bool
leq x y = x < y || x == y

不进行类型检查。

就您关于强制特定层次结构的超类的观点而言,您错过了目标。

这种“强制”实际上是类型类的一个基本特征。类型类是“由设计决定的”,并且在给定的 Haskell 程序中(其中“程序”包括所有库,包括程序使用的 base),对于特定类型,特定类型类只能有一个实例.这种性质称为连贯性。 (即使有语言扩展IncohorentInstances,它也被认为是非常危险的,并且只应在特定类型的特定类型类的所有可能实例在功能上等效时使用。)

这种设计决策会带来一定的成本,但也会带来许多好处。 Edward Kmett 从大约 14:25 开始在 this video 中详细讨论了这一点。特别是,他将 Haskell 的设计一致性类型类与 Scala 的非一致性设计隐式进行了比较,并将 Scala 方法带来的增强功能与“哑数据类型”的可重用性(和重构好处) Haskell 方法。

因此,在设计空间中有足够的空间用于连贯类型类和不连贯的隐式,而 Haskell 的方法不一定是正确的。

但是,由于 Haskell 选择了一致的类型类,因此拥有特定的层次结构没有“成本”:

class Functor a => Monad a

因为对于特定类型,例如[]MyNewMonadDataType,无论如何只能有一个Monad 和一个Functor 实例。超类关系引入了一个要求,即任何具有Monad 实例的类型都必须具有Functor 实例,但它并不限制Functor 实例的选择因为你一开始就没有选择。或者更确切地说,您的选择是在 Functor [] 实例为零和恰好一个之间。

请注意,这与Monad 类型是否只有一个合理的Functor 实例的问题不同。原则上,我们可以用不兼容的FunctorMonad 实例定义一个违法数据类型。无论Functor 是否是Monad 的超类,我们仍然会被限制在整个程序中使用Functor MyType 实例和Monad MyType 实例。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-06-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多