【问题标题】:How to update a list element in Haskell如何更新 Haskell 中的列表元素
【发布时间】:2021-12-25 04:23:30
【问题描述】:

来自 Python 和 Javascript 背景,我真的不明白为什么在 Haskell 中更新列表的元素并不简单。我的意思是,我为什么不能写像myList[0] = "newValue" 这样简单的东西?

所以,我有两个问题:

  1. 为什么在 Haskell 中更新列表的元素不简单?我希望能在这里得到一些理论上的回应。
  2. 如何更新列表中的元素?到目前为止,我的方法是在更新我感兴趣的元素的同时重新创建整个列表。但这种方法似乎并不高效。

【问题讨论】:

  • But this approach doesn't seem performant.。许多来自其他语言的人认为,不可变的数据结构,以及随之而来的需要返回新结构而不是更新的函数,会导致很大的性能损失。但通常情况并非如此,因为新旧数据结构可以共享内存。例如,更新链表的第一个元素并不意味着创建一个全新的副本,它只是意味着创建一个新的第一个节点,该节点指向与原始节点相同的下一个节点。如果数据结构是可变的,这将更加困难。
  • 投票结束,问题没有重点。

标签: list haskell


【解决方案1】:

这与列表无关。有意地,在 Haskell 中根本不可能更新任何值。

为什么?嗯,这就是语言的设计方式。您可能会问为什么命令式语言允许更新值。我的意思是,你写

x = 3

然后你写

x = 4

wut?那么第一个是谎言还是什么?当然,我们应该明确指出我们指的是两个不同的时间变量。这只是询问错误!当然,仅仅保存一些字符并启用一些也可以通过其他更安全的方式实现的低级优化是不值得的吗?
...对吧,不是吗?

即使在命令式语言中,更新(链接)列表中的值实际上也没有多大意义——无论如何您都需要遍历 O (n) 个参数甚至到达应该改变的那个。创建一个全新的列表平均只需要更改旧列表的两倍,但与命令式更新不同,您无需担心其他东西是否仍然需要旧版本,因为您永远不会干扰它。

无论如何,链表通常都很慢,所以担心 2× 因子也没有多大意义。但是,GHC 经常完全优化列表,因此它们实际上根本不会在内存中构建,而这是编译器更难保证的事情之一,如果它不得不担心某人在其他地方改变列表。

更新 数组 中的元素,这是另一回事。实际上,更新数组中的值在 Haskell 中也很常见,因为这会带来重要的性能提升。 更新数组类型的值仍然是不可能的,但是可以通过对一元可变数组的引用来更新一个值。这最终看起来与在命令式语言中更新数组的方式非常相似,尽管它通常有点冗长,因为所有数组工具都需要从库中导入,而不是使用内置语法。

import qualified Data.Vector.Mutable as VM

main :: IO ()
main = do
  myArray <- VM.generate 37 (const "this element isn't set yet")
  print =<< VM.read myArray 0
  VM.write myArray 0 "newValue"
  print =<< VM.read myArray 0      

等效 Python:

def main():
    myArray = ["this element isn't set yet"] * 37
    print(myArray[0])
    myArray[0] = "newValue"
    print(myArray[0])

然而,很多时候您并不真正需要更新元素。事实上,我们马上就看到了这里的一个问题:你正在索引到一个数组中。这意味着您需要确保索引确实存在。在命令式语言中,这很常见,以至于您几乎不会去想它,但在 Haskell 中,我们真的更喜欢 total 代码,即即使我们不小心交换了两个变量(如两个不同的循环索引),也不应该给出运行时错误,但最好是编译器错误。

通常情况下,如果您更新单个元素,那么您也会更新其他元素。事实上,您经常会按顺序更新它们中的所有,然后与简单地从头开始构建一个包含更新的新列表并没有太大的优势。马上价值观。使用这种方法,几乎​​不会出错,至少在运行时不会出错。


这里有一个很大的警告:如果其他人仍在使用旧列表,则意味着垃圾收集器无法回收内存。这就是为什么在 Haskell 中很容易出现内存泄漏的原因 - IMO 是该语言的一个最大问题。

【讨论】:

  • 更新链表的节点是 O(1) 时间和0 aux 空间,当你已经有了指向节点的指针时——在许多算法中,你做。复制是 O(n)O(n)。在最佳时间打印(或搜索)n 长链表的所有排列(使用“缩小域”)是 0 aux space with links mutationO(n^2) 辅助空间(除了 O(n^2) i>O(n) 空间用于 n 嵌套循环/递归调用,当然还有 n 长排列本身)。就地更新链表可能是一个优势。
  • 这就是我正在寻找的解释类型。谢谢你非常详细的回答!
【解决方案2】:

感觉应该很熟悉:Python 字符串的行为方式相同。你不能这样做 myString[0] = 'H',即使在 C 等其他语言中也可以。

这使编写代码更容易,并有助于防止错误,因为您不必经常注意谁修改了哪个字符串。

如果你确实想“修改”一个 Python 字符串?你不会三思而后行:

myString = 'H' + myString[1:]

当然,如果您需要传播此类更改,则需要您稍微重新构建程序,但总体而言,它是有用且有效的。出于同样的原因,Python 对数字和元组也是如此。

Haskell 旨在更安全,因此对列表也是如此。 (还有地图。还有场景。还有其他一切。)

【讨论】:

  • 虽然不太相似。使某些类型可变而其他类型几乎不会使任何东西更可靠,它只会使语义不那么一致。只有当你限制某些东西的类型是不可变的,你才能获得一些东西。在 Python 中,不可变类型的重点主要在于您可以将它们用作哈希映射的键。但我会说这是一个 hack,如果有一种通用方法可以使任意类型的值不可变,就像在 C++ 中那样。
  • 另外,Python 中的 'H' + myString[1:] 比 Haskell 中的 'H' : tail myString 差得多。在 Python 中,它分配一个完整的新字符串,复制大部分旧内容,然后 GCs 旧内容。在 Haskell 中,它只是将原始列表添加到新字母前面,尽管这依赖于链表实现,在大多数意义上实际上比 Python 的打包数组字符串更糟糕。对于严肃的文本处理,Haskell has a dedicated library.
猜你喜欢
  • 1970-01-01
  • 2017-07-31
  • 2011-08-24
  • 1970-01-01
  • 2021-10-30
  • 1970-01-01
  • 1970-01-01
  • 2021-03-02
  • 2023-03-31
相关资源
最近更新 更多