4

考虑一下我用来解决欧拉问题 58 的代码:

diagNums = go skips 2
    where go (s:skips) x = let x' = x+s
                           in x':go skips (x'+1)

squareDiagDeltas = go diagNums
    where go xs = let (h,r) = splitAt 4 xs
                  in h:go r

我不喜欢第二个函数中的模式匹配。它看起来比必要的复杂!这对我来说经常出现。这里,splitAt返回一个元组,所以我必须先解构它,然后才能递归。当我的递归本身返回一个我想要修改的元组时,同样的模式可能会更烦人。考虑:

f n = go [1..n]
    where go [] = (0,0)
          go (x:xs) = let (y,z) = go xs
                      in (y+x, z-x)

与漂亮而简单的递归相比:

f n = go [1..n]
    where go [] = 0
          go (x:xs) = x+go xs

当然,这里的函数纯粹是胡说八道,可以用完全不同的更好的方式编写。但我的观点是,每当我需要通过递归返回多个值时,就会出现模式匹配的需求。

有什么方法可以避免这种情况,也许是通过使用Applicative或类似的方法?或者你会认为这种风格是惯用的吗?

4

3 回答 3

6

首先,这种风格实际上是相当地道的。由于您对两个不同的值做两件事,因此存在一些不可简化的复杂性;实际的模式匹配本身并没有引入太多。此外,我个人发现显式风格在大多数情况下都非常易读。

但是,还有另一种选择。Control.Arrow有一堆函数用于处理元组。由于函数箭头->也是一个Arrow,所有这些都适用于普通函数。

所以你可以重写你的第二个例子,使用(***)组合两个函数来处理元组。该运算符具有以下类型:

(***) :: a b c -> a b' c' -> a (b, b') (c, c')

如果我们用 替换a->我们得到:

(***) :: (b -> c) -> (b' -> c') -> ((b, b') -> (c, c'))

因此,您可以将(+ x)和组合(- x)成一个函数(+ x) *** (- x)。这相当于:

\ (a, b) -> (a + x, b - x)

然后你可以在你的递归中使用它。不幸的是,该-运算符很愚蠢并且不能分段工作,因此您必须使用 lambda 编写它:

(+ x) *** (\ a -> a - x) $ go xs 

您显然可以想象使用任何其他运算符,所有这些都不是那么愚蠢:)。

老实说,我认为这个版本的可读性不如原版。但是,在其他情况下,该***版本可能更具可读性,因此了解它很有用。特别是,如果您传递(+ x) *** (- x)给高阶函数而不是立即应用它,我认为该***版本会比显式 lambda 更好。

于 2013-06-01T08:37:33.253 回答
4

我同意hammar的观点,unfoldr就是去这里的路

您还可以摆脱模式匹配diagNums

diagNums = go skips 2
    where go (s:skips) x = let x' = x+s
                           in x':go skips (x'+1)

递归使得我们很难判断这里发生了什么,所以让我们深入研究一下。

假设skips = s0 : s1 : s2 : s3 : ...,那么我们有:

diagNums = go skips 2
         = go (s0 : s1 : s2 : s3 : ...) 2 
         = s0+2 : go (s1 : s2 : s3 : ... ) (s0+3)
         = s0+2 : s0+s1+3 : go (s2 : s3 : ... ) (s0+s1+4) 
         = s0+2 : s0+s1+3 : s0+s1+s2+4 : go (s3 : ... ) (s0+s1+s2+5) 
         = s0+2 : s0+s1+3 : s0+s1+s2+4 : s0+s1+s2+s3+5 : go (...) (s0+s1+s2+s3+6) 

这使得发生了什么更清楚了,我们得到了两个序列的总和,这很容易计算使用zipWith (+)

diagNums = zipWith (+) [2,3,4,5,...] [s0, s0+s1, s0+s1+s2, s0+s1+s2+s3,...] 

所以现在我们只需要找到一种更好的方法来计算 的部分和skips,这对 很有用scanl1

scanl1 (+) skips = s0 : s0+s1 : s0+s1+s2 : s0+s1+s2+s3 : ...

留下一个(IMO)更容易理解的定义diagNums

diagNums = zipWith (+) [2..] $ scanl1 (+) skips
于 2013-06-01T15:30:54.423 回答
4

我同意 Tikhon Jelvis 的观点,你的版本没有任何问题。就像他说的,使用组合子 fromControl.Arrow对高阶函数很有用。f您可以使用折叠编写:

f n = foldr (\x -> (+ x) *** subtract x) (0,0) [1..n]

如果你真的想摆脱letin squareDiagDeltas(我不确定我会不会),你可以使用second,因为你只是修改元组的第二个元素:

squareDiagDeltas = go diagNums
  where go = uncurry (:) . second go . splitAt 4
于 2013-06-01T08:54:17.050 回答