功能上解决问题:如何使用Haskell?

时间:2013-12-10 21:24:39

标签: haskell recursion functional-programming

我正在尝试解决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做事的正确方法吗?它不仅仅是命令式编程吗?

编辑:另外,你如何在这里避免额外的功能?

3 个答案:

答案 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。然后你得出结论,额外的功能是正常的。还有广泛使用的方法来隐藏局部区域的功能。这本书可能看起来很无聊,但在前面的章节中,她会习惯于解决问题的功能方法。

有些任务需要递归方法。但是,例如,如果你使用尾递归(这是经常被无缘无故称赞),那么你会注意到这只是通常的迭代。通常使用“额外功能”来隐藏迭代变量(哦......单词变量不太合适,很可能是参数)。