Haskell中的列表:数据类型还是抽象数据类型?

时间:2009-12-21 19:51:46

标签: list haskell linked-list types abstract-data-type

据我所知,Haskell中的列表类型是使用链表在内部实现的。但是,该语言的用户无法查看实现的详细信息,也无法修改组成链接列表的“链接”以允许其指向不同的内存地址。我想这是在内部完成的。

那么,如何在Haskell中限定列表类型?它是“数据类型”还是“抽象数据类型”?那个实现的链表类型是什么?

此外,由于Prelude提供的列表类型不是链表类型,如何实现基本链表功能?

例如,这段代码旨在在列表的索引n处添加元素a:

add [] acc _ _ = reverse acc
add (x:xs) acc 0 a = add xs (x:a:acc) (-1) a 
add (x:xs) acc n a = add xs (x:acc) (n-1) a

使用“真实”链表,添加元素只需修改指向内存地址的指针即可。这在Haskell中是不可能的(或者是它?),因此问题是:我的实现是将一个元素添加到列表中最好的一个,或者我错过了什么(使用reverse函数是,我认为,特别难看,但是可以不做吗?)

如果我说的话有误,请不要犹豫,纠正我,谢谢你的时间。

6 个答案:

答案 0 :(得分:10)

你对数据结构的可变性感到困惑。它一个正确的列表 - 只是你不允许修改的列表。 Haskell纯粹是功能性的,意味着值是不变的 - 您不能更改列表中的项目,而不能将数字2转换为3.相反,您可以执行计算以创建具有所需更改的新值。

您可以通过这种方式最简单地定义该功能:

add ls idx el = take idx ls ++ el : drop idx ls

列表el : drop idx ls重用原始列表的尾部,因此您只需要生成一个最新为idx的新列表(这是take函数的作用)。如果你想使用显式递归来实现它,你可以像这样定义它:

add ls 0 el   = el : ls
add (x:xs) idx el
  | idx < 0   = error "Negative index for add"
  | otherwise = x : add xs (idx - 1) el
add [] _ el   = [el]

这会以相同的方式重复使用列表的尾部(在第一种情况下是el : ls)。

由于您似乎无法查看这是一个链接列表,所以让我们清楚链接列表是什么:它是一个由单元格组成的数据结构,其中每个单元格都有一个值和对下一个项目的引用。在C中,它可以定义为:

struct ListCell {
void *value; /* This is the head */
struct ListCell *next; /* This is the tail */
}

在Lisp中,它被定义为(head . tail),其中head是值,tail是对下一个项目的引用。

在Haskell中,它被定义为data [] a = [] | a : [a],其中a是值,[a]是对下一个项目的引用。

如您所见,这些数据结构都是等效的。唯一的区别是在C和Lisp中,它们不是纯粹的功能,头部和尾部值是你可以改变的东西。在Haskell中,你无法改变它们。

答案 1 :(得分:8)

Haskell是一种纯函数式编程语言。这意味着根本无法进行任何改变。

列表是非抽象类型,它只是一个链表。

你可以想到以这种方式定义它们:

data [a] = a : [a] | []

这正是链接列表的定义方式 - 头部元素和(指向)其余部分。

请注意,这在内部没有区别 - 如果您想要更高效的类型,请使用SequenceArray。 (但由于不允许进行任何更改,因此您无需实际复制列表以区分副本,这可能是性能增益而不是命令式语言)

答案 2 :(得分:5)

在Haskell中,“数据类型”和“抽象类型”是艺术术语:

  • “数据类型”(非抽象)具有可见值构造函数,您可以在case表达式或函数定义中进行模式匹配。

  • “抽象类型”没有可见值构造函数,因此您无法对该类型的值进行模式匹配。

如果类型为a[a]a列表)是数据类型,因为您可以在可见构造函数上进行模式匹配(书面{ {1}})和nil(书面:)。抽象类型的一个示例是[],您无法通过模式匹配来解构。

答案 3 :(得分:4)

您的代码可能有效,但绝对不是最佳选择。假设您要在索引0处插入项目。示例:

add [200, 300, 400] [] 0 100

如果您遵循此推导,最终得到:

add [200, 300, 400] [] 0 100
add [300, 400] (200:100:[]) (-1) 100 
add [400] (300:[200, 100]) (-2) 300 
add [] (400:[300, 200, 100]) (-3) 400 
reverse [400, 300, 200, 100]
[100, 200, 300, 400]

但我们只是在列表的开头添加一个项目!这样的操作很简单!这是(:)

add [200, 300, 400] [] 0 100
100:[200, 300, 400]
[100, 200, 300, 400]

考虑一下这个列表中有多少需要反转。

您询问运行时是否修改了链表中的指针。因为Haskell中的列表是不可变的,所以没有人(甚至不是运行时)修改链表中的指针。这就是为什么,例如,将项目附加到列表的前面是很便宜的,但是在列表的后面附加元素是昂贵的。将项目追加到列表的前面时,可以重新使用所有现有列表。但是当你在最后追加一个项目时,它必须建立一个全新的链表。为了使列表前面的操作便宜,需要数据的不变性。

答案 4 :(得分:3)

Re:在List的末尾添加一个元素,我建议使用(++)运算符和splitAt函数:

add xs a n = beg ++ (a : end)
  where
    (beg, end) = splitAt n xs

List是链接列表,但它是只读的。您无法修改List - 而是创建一个具有所需元素的新List结构。我还没看过,但this book可能会解决你的基本问题。

HTH

答案 5 :(得分:1)

编译器可以自由选择列表所需的任何内部表示。而在实践中它实际上确实有所不同。显然,列表“[1 ..]”并未作为一系列经典的cons单元实现。

实际上,一个惰性列表存储为thunk,它评估为包含下一个值和下一个thunk的cons单元格(thunk基本上是一个函数指针加上函数的参数,它被实际值替换一次该函数被调用)。另一方面,如果编译器中的严格性分析器可以证明整个列表将始终被评估,那么编译器只会将整个列表创建为一系列缺点单元格。