我正在尝试解决H99中的一个问题: 将列表拆分为两部分;给出了第一部分的长度。
不要使用任何预定义的谓词。
示例:
> (split '(a b c d e f g h i k) 3)
( (A B C) (D E F G H I K))
我可以快速找到解决方案:
split'::[a]->Int->Int->[a]->[[a]]
split' [] _ _ _ = []
split' (x:xs) y z w = if y == z then [w,xs] else split' xs y (z+1) (w++[x])
split::[a]->Int->[[a]]
split x y = split' x y 0 []
我的问题是我正在做的只是以递归格式重写循环版本。这是你在Haskell做事的正确方法吗?它不仅仅是命令式编程吗?
编辑:另外,你如何在这里避免额外的功能?
答案 0 :(得分:6)
你可以经常将命令式解决方案转换为Haskell,但是你是对的,你通常希望找到一个更自然的递归语句。特别是对于这一点,基础案例和归纳案例的推理可能非常有用。那么你的基本案例是什么?为什么,当拆分位置为0时:
split x 0 = ([], x)
通过将列表的第一个元素添加到用n-1分割的结果上,可以构建归纳案例:
split (x:xs) n = (x:left, right)
where (left, right) = split xs (n-1)
这可能不会表现得很好(它可能没有你想象的那么糟糕),但它说明了我第一次遇到问题并希望在功能上接近它时的思考过程。
编辑:另一个更依赖Prelude的解决方案可能是:
split l n = (take n l, drop n l)
答案 1 :(得分:3)
它实际上与命令式编程不同,每个函数调用都避免了任何副作用,它们只是简单的表达式。但我对你的代码有一个建议
split :: Int -> [a] -> ([a], [a])
split p xs = go p ([], xs)
where go 0 (xs, ys) = (reverse xs, ys)
go n (xs, y:ys) = go (n-1) (y : xs, ys)
那么我们如何声明我们只返回两个事物([a], [a])
而不是一个事物列表(这有点误导)并且我们已经将我们的尾递归调用约束在本地范围内
我也在使用模式匹配,这是一种在Haskell中编写递归函数的更惯用的方法,当用{0}调用go
时,则运行第一种情况。编写递归函数通常更令人愉快,因为你可以使用模式匹配而不是if语句。
最后这更有效,因为++
在第一个列表的长度上是线性的,这意味着函数的复杂性是二次而不是线性的。与Daniel的解决方案不同,这种方法也是尾递归的,这对处理任何大型列表都很重要。
TLDR:两个版本都是功能样式,避免变异,使用递归而不是循环。但我提出的版本更多的是Haskell-ish而且速度稍快。
此解决方案使用尾递归,这在Haskell 中并不总是必不可少的,但在这种情况下,当您使用结果列表时,这有用,但在其他时候实际上是一件坏事。例如,map
不是尾递归,但如果是,则无法在无限列表中使用它!
在这种情况下,我们可以使用尾递归,因为整数总是有限的。但是,如果我们只使用列表的第一个元素,Daniel的解决方案很多更快,因为它会懒惰地生成列表。另一方面,如果我们使用整个列表,我的解决方案会更快。
答案 2 :(得分:-1)
split'::[a]->Int->([a],[a])
split' [] _ = ([],[])
split' xs 0 = ([],xs)
split' (x:xs) n = (x:(fst splitResult),snd splitResult)
where splitResult = split' xs (n-1)
您似乎已经展示了更好解决方案的示例。
我建议你阅读SICP。然后你得出结论,额外的功能是正常的。还有广泛使用的方法来隐藏局部区域的功能。这本书可能看起来很无聊,但在前面的章节中,她会习惯于解决问题的功能方法。
有些任务需要递归方法。但是,例如,如果你使用尾递归(这是经常被无缘无故称赞),那么你会注意到这只是通常的迭代。通常使用“额外功能”来隐藏迭代变量(哦......单词变量不太合适,很可能是参数)。