在Haskell中,为什么没有一个可以像列表一样操作的TypeClass?

43

我正在阅读Learn You a Haskell,想知道为什么这么多东西都像列表一样,而Prelude中没有使用类型类的本机功能来设置这个:

":的字节串版本称为cons。它接受一个字节和一个字节串,并将字节放在开头。但它是惰性的,所以即使字节串中的第一个块没有满,它也会创建一个新的块。如果要在字节串的开头插入大量字节,最好使用严格版本的cons'。"

为什么没有一个TypeClass listable或其他提供:函数来统一Data.ByteStringData.ListData.ByteString.Lazy等呢?这是有原因的,还是只是Haskell遗留问题的一部分?使用:作为例子有点轻描淡写,LYAH中还有这样一段话:

否则,bytestring模块有很多与Data.List中类似的函数,包括但不限于head、tail、init、null、length、map、reverse、foldl、foldr、concat、takeWhile、filter等。


你能解释一下你如何想象这个工作的吗?显然不可能有一个类型类,既有ByteString又有[]作为实例,因为[]具有* -> *的种类,而ByteString只是* - Travis Brown
@Travis Brown:你可以使用一个简单的参数化 newtype 包装器来实现。这个方法已经被多次重新发明,但是这里有一个例子 http://hackage.haskell.org/packages/archive/iteratee/0.2.1/doc/html/Data-Iteratee-WrappedByteString.html - Anthony
7
如果有一个库可以完成你想要的功能,那么为什么需要将它包含在语言本身中呢? - Robert Harvey
1
@Robert,如果我有的话,我会给你10个赞。 - luqui
11
@Robert,@luqui,我不理解你们的问题,或者说你们的问题没有意义...假设没有提供 = 的类型类,并且预定义了每种数值类型的不同函数来测试相等性;此外,每个库都遵循相同的约定。那么,难道不应该在“语言本身”中使用类型类吗?如果你能回答这个问题,你可能已经准备好回答我的问题了。 - Evan Carroll
7个回答

31
ListLike包似乎提供了您需要的内容。我从未理解为什么它不太受欢迎。
除了ListLike之外,这个函数没有被实现到Prelude中的原因是,如果不调用一些语言扩展(多参数类型类和fundeps或关联类型),就无法很好地完成它。有三种容器需要考虑:
1. 完全不关心其元素的容器(例如[])。 2. 仅针对特定元素实现的容器(例如bytestrings)。 3. 对元素具有多态性但需要上下文的容器(例如Data.Vector.Storable,它将保持任何具有可存储实例的类型)。
以下是一个非常基本的ListLike风格的类,而不使用任何扩展:
class Listable container where
  head :: container a -> a

instance Listable [] where
  head (x:xs) = x

instance Listable ByteString where --compiler error, wrong kind

instance Listable SV.Vector where
  head v = SV.head    --compiler error, can't deduce context (Storable a)

这里的container类型为*->*,但对于字节数组并不适用,因为它们不允许任意类型;它们的类型为*。此外,对于Data.Vector.Storable向量也不适用,因为该类不包括上下文(即Storable限制)。
你可以通过更改类定义来解决这个问题:
class ListableMPTC container elem | container -> elem where

或者

class ListableAT container where
  type Elem container :: *

现在container的类型为*,它是一个完全应用的类型构造函数。也就是说,您的实例看起来像这样:
instance ListableMPTC [a] a where

但您不再是Haskell98。

这就是为什么即使是简单的可列表类型接口也是非平凡的原因;当您需要考虑不同的集合语义(例如队列)时,它会变得更加困难。另一个真正大的挑战是可变与不可变数据。到目前为止,我见过的每一次尝试(除了一次)都通过创建一个可变接口和一个不可变接口来解决该问题。我知道的唯一统一两者的接口是令人费解的,调用了一堆扩展,并且性能相当差。

附录:字节串

我完全是在猜测,但我认为我们被演化的产物所束缚,必须使用字节串来解决低性能I/O操作的问题,而使用Ptr Word8与IO系统调用进行交互是有意义的。指针操作需要Storable,并且最可能使多态性起作用所需的扩展(如上所述)当时还不可用。现在很难克服它们的动力。类似的容器具有多态性肯定是可能的,storablevector包实现了这一点,但它远远不如bytestring流行。

字节串是否可以没有任何元素限制而是多态的?我认为Haskell最接近这个问题的解决方案是Array类型。与bytestring相比,这不太适合低级别的I/O,因为需要将数据从指针解包到数组的内部格式中。此外,数据是装箱的,这会增加显着的空间开销。如果您想要未装箱的存储(更少的空间)和与C的高效交互,则应使用指针。一旦您拥有了一个Ptr,您就需要Storable,然后您需要在类型类中包含元素类型,因此您只剩下扩展要求。

话虽如此,我认为对于任何单个容器实现来说,只要有适当的扩展可用,这基本上是一个已解决的问题(除了可变/不可变API)。现在更难的部分是提出一组合理的类,可用于许多不同类型的结构(列表、数组、队列等),并且足够灵活以便有用。我个人认为这应该相对简单,但我可能错了。


1
我对Haskell还很陌生,请轻一点:为什么ByteString的种类是*?这似乎也太随意了——为什么不让它具有多态性呢?我现在可以理解目前的推理,但假设一个8位字节是完全不必要的假设吗?为什么不允许ByteString[Word7]或者使用类型同义词别名使ByteString更像String... 话虽如此,我最喜欢这个答案,因为它试图解释为什么这并不是微不足道的。更新Haskell语言以标准化GHC pragma会使这变得微不足道吗? - Evan Carroll
@Evan:编辑了我的回复以回答关于字节串的问题。 - John L

19

这样的类的主要问题是即使它存在,它也只会提供一种表面上的相似性。

使用不同结构构建的相同算法的渐进复杂度会有极大差异。

对于严格的字节串,用cons来构建它们是可怕的,因为每次添加另一个Char时,您最终会复制整个字符串。在列表上的这个O(1)操作将其转换为Bytestring上的O(n)操作。

当您实现可能首先想到的第一个算法map时,这会导致O(n^2)的行为,而使用cons构建列表或Data.Sequence.Seq是线性时间,并且可以在字节串或向量上通过一点思考以O(n)实现。

结果是,在这种情况下,这样一个类的实用性更多的是表面的而非实际的。

我并不是说找不到好的设计,但这样的设计难以使用和优化,可用的版本可能不会成为Haskell 98。

我在我的keys包中挤出了这个设计空间的部分内容,该包提供了许多用于索引容器等功能的函数,但我故意避免提供类似列表的API a.)因为已经做过而且成功甚微,b.)因为上述渐进性问题。

简而言之 当底层操作的渐进复杂度发生变化时,通常希望以非常不同的方式实现算法。


1
这是一个非常好的观点,我之前没有考虑过。但是经过思考,我不确定我完全同意,因为Haskell基础库中已经存在许多函数,它们的渐近率取决于传入的值。甚至+根据您传入的是Int还是Integer而有不同的渐近率,如果您创建了一个实现+的自定义矩阵类型,则可能会得到O(n)+。此外,许多其他语言都具有具有不同渐近率的函数(例如Java ArrayList / LinkedList)。我真的认为这种效用远非肤浅,也许不完美。 - semicolon

14

提供将Data.ByteString、Data.List、Data.ByteString.Lazy等数据类型统一的函数是什么?

尝试设计一个好的a)序列接口和b)容器接口,但是将不同种类的数据类型,带有不同的类型约束进行统一通常会使结果非标准化,以至于很难想象将它们放入基本库。对于数组也是如此,虽然Vector包现在拥有相当通用的接口(基于相关数据类型)。

还有一些项目试图将这些半相关的数据类型用单一接口统一起来,因此我对能够很快看到结果持有期望。容器类型同理。然而,结果并不会很简单。


1
数据可折叠(Data.Foldable)是一个合适的解决方案吗? - Phil
3
@phil:Data.Foldable 和 Data.Traversable 都很好,但它们都没有提供完整的接口。 - John L
2
我对这方面的进展不太乐观。我认为当前大多数努力存在两个重大缺陷。第一个是人们想以它们并不特别适合的方式重用现有的类型类(Monoid 是一个常见的例子)。第二个是我目前看到的大多数尝试都涉及到大而笨重的类(例如 ListLike),这使得实例编写者在他们的实例无法完全实现所有所需方法时受到限制。我不认为解决方案是不可能的,但它肯定是非常复杂的。 - John L
@JohnL 这在13年后有改变吗? - lo tolmencre

0

有两个类型类被称为FoldableTraversable,旨在抽象出列表和其他顺序数据结构的一些常见行为。然而,并非所有数据结构都具有这些实例,我不知道它们是否对编译器足够透明,以便它仍然可以对它们进行优化(有人知道吗?)

来源:Foldable and Traversable
另请参阅 为什么Haskell缺少“显而易见”的类型类 的答案。


0

实际上,GHC.Exts 中有一个 OverloadedLists 扩展和一个相应的 IsList 类。


-1

在Haskell中,为类似列表的数据创建类型类并没有太多价值。为什么?因为惰性求值。您只需编写将数据转换为列表的函数,然后使用该列表即可。列表只会在需要其子列表和元素时构建,并且只要前缀没有引用,它们的内存就可以被回收。

提供通用toList函数的类型类确实有价值,但是这已经存在于Data.Foldable中。

因此,基本上解决方案是实现Data.Foldable并使用其toList函数。


1
那只有帮助消耗数据。问题至少是在询问通用构造函数。Foldable并不能提供这样的功能。 - C. A. McCann
1
我认为,当你只是想基本上使用“(:)”作为语法上的便利时,将一个Vector转换为列表再转回来并不是一种可行的选择。 - dflemstr

-2

ByteString 不是一个通用类型。

在其他语言中,有像 Sequence 这样的东西,适用于所有类似列表的数据结构。 我认为这可以工作,并且可以通过正确的扩展来实现:

class Seq a b | a -> b where
  head :: a -> b
  isTail :: a -> Bool

# ([a]) is a sequence of a's
instance Seq [a] a where
  head (x:xs) = x
  isTail = (== [])

# ByteString is a sequence of chars
instance Seq ByteString Char

或者尝试这个?

type BS a = ByteString
instance List BS

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