Haskell 避免递归中的模式匹配
考虑我用来解决Euler问题58的代码:Haskell 避免递归中的模式匹配,haskell,applicative,Haskell,Applicative,考虑我用来解决Euler问题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 我不喜欢第二个函数中的模式匹
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
或类似的方法?或者你认为这种风格是惯用的吗?因为你对两种不同的价值观做了两件事,所以有一些不可简化的复杂性;实际的模式匹配本身不会引入太多内容。此外,我个人觉得这种直白的风格在大多数时候都非常可读
然而,还有一种选择<代码>控件。箭头有一系列用于处理元组的函数。由于功能箭头->
也是箭头
,因此所有这些都适用于正常功能
因此,您可以使用(***)
重写第二个示例,将两个函数组合起来处理元组。此运算符具有以下类型:
(***) :: 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更好。我同意Tikhon Jelvis的观点,即您的版本没有问题。正如他所说,使用Control.Arrow中的组合符对高阶函数很有用。您可以使用折页书写f
:
f n = foldr (\x -> (+ x) *** subtract x) (0,0) [1..n]
如果您真的想去掉squarediagdelatas
中的let
(我不确定我会这么做),可以使用second
,因为您只修改元组的第二个元素:
squareDiagDeltas = go diagNums
where go = uncurry (:) . second go . splitAt 4
我同意
您还可以在诊断中取消模式匹配:
diagNums = go skips 2
where go (s:skips) x = let x' = x+s
in x':go skips (x'+1)
diagNums = zipWith (+) [2..] $ scanl1 (+) skips
递归使我们很难判断这里发生了什么,所以让我们
深入研究它
假设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(+)
计算:
所以现在我们只需要找到一种更好的方法来计算跳过的部分和
,这对于:
为诊断留下一个(IMO)更容易理解的定义
:
diagNums = go skips 2
where go (s:skips) x = let x' = x+s
in x':go skips (x'+1)
diagNums = zipWith (+) [2..] $ scanl1 (+) skips
可能会用(+(-x))
或(减去x)
替换a
lambda,或者存在一些问题?谢谢!:)@是的,你也可以做这两件事。我不喜欢任何一种选择:P,所以是箭头。我突然想起了这个想法。你是对的,现在还不清楚它是否比原来的好,但它最直接、最清楚地回答了我的问题,所以我接受你的回答。另一个选项:squareDiagDeltas=unfover(Just.splitAt 4)diagNums
它通过第二次尝试读起来既有趣又有创意,所以+1,谢谢!