据我所知,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
函数是,我认为,特别难看,但是可以不做吗?)
如果我说的话有误,请不要犹豫,纠正我,谢谢你的时间。
答案 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] | []
这正是链接列表的定义方式 - 头部元素和(指向)其余部分。
请注意,这在内部没有区别 - 如果您想要更高效的类型,请使用Sequence
或Array
。 (但由于不允许进行任何更改,因此您无需实际复制列表以区分副本,这可能是性能增益而不是命令式语言)
答案 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基本上是一个函数指针加上函数的参数,它被实际值替换一次该函数被调用)。另一方面,如果编译器中的严格性分析器可以证明整个列表将始终被评估,那么编译器只会将整个列表创建为一系列缺点单元格。