我正在学习Haskell并尝试使用列表。 我写了一个函数,它按照特定模式转换列表,我无法检查它是否现在有效,但我想是的。
这个函数不是尾调用函数,所以我认为用一个大的列表计算这个函数会很糟糕:
transform :: [Int] -> [Int]
transform list = case list of
(1:0:1:[]) -> [1,1,1,1]
(1:1:[]) -> [1,0,1]
[1] -> [1,1]
(1:0:1:0:s) -> 1:1:1:1: (transform s)
(1:1:0:s) -> 1:0:1: (transform s)
(1:0:s) -> 1:1: (transform s)
(0:s) -> 0: (transform s)
所以我想到了另一个功能,它会“更好”:
transform = reverse . aux []
where
aux buff (1:0:[1]) = (1:1:1:1:buff)
aux buff (1:[1]) = (1:0:1:buff)
aux buff [1] = (1:1:buff)
aux buff (1:0:1:0:s) = aux (1:1:1:1:buff) s
aux buff (1:1:0:s) = aux (1:0:1:buff) s
aux buff (1:0:s) = aux (1:1:buff) s
aux buff (0:s) = aux (0:buff) s
问题是我不知道它是如何编译的,如果我错了第二个函数。你能解释一下列表是如何工作的吗?最后使用(++)或反转列表是否更好?
提前感谢您的回答
答案 0 :(得分:7)
第一个功能非常好,在我看来比第二个更好。
原因是懒惰。如果代码中有transform someList
表达式,则除非您需要,否则不会评估结果列表。特别是列表将仅根据需要评估;与print $ take 10 $ transform xs
相比,print $ take 20 $ transform xs
的工作量会减少。
严格的语言transform
确实会阻塞堆栈,因为它必须在返回任何使用之前评估整个列表(以非尾递归的方式)。在Haskell中transform (0:xs)
求值为0 : transform xs
,这是一个可用的部分结果。我们可以在不接触尾部的情况下检查该结果的头部。堆栈溢出也没有危险:在列表的尾部,任何时候最多只有一个未评估的thunk(如前例中的transform xs
)。如果你需要更多的元素,thunk将被推回更远,并且前一个thunk的堆栈帧可以被垃圾收集。
如果我们总是完全评估列表,那么这两个函数的性能应该是相似的,或者即使这样,由于缺少逆转或额外的++
- s,懒惰版本可能会更快一些。因此,通过切换到第二个功能,我们会失去懒惰并且不会获得额外的性能。
答案 1 :(得分:3)
你的第一个版本对我来说看起来好多了 1 。这很好,它不是尾递归的:你不希望它是尾递归的,你希望它是懒惰的。第二个版本在不处理整个输入列表的情况下甚至不能生成单个元素,因为为了reverse
aux
的结果,必须知道整个aux
。但是,
take 10 . transform $ cycle [1,0,0,1,1,1]
可以正常使用transform
的第一个定义,因为您只需要根据需要使用尽可能多的列表来做出决定。
1 但请注意(1:0:1:[])
只是[1,0,1]
。