原来zipWith有三种情况:
- 当第一个列表为空时
- 当第二个列表为空时
- 当两个列表都不为空时
第三种情况在参数尾部递归调用zipWith,再次进行情况分析。
在您的定义中,您只有一种情况 - 列表推导,因此任何递归调用都将返回到该情况。如果没有案例分析,您可能会在这里永远循环:
>>> let myZipWith f xs ys = [ f x y | (x,y) <- myZipWith f xs ys ]
>>> myZipWith (,) [] []
^CInterrupted.
此外,因为您在递归调用中使用 f,但要求递归输出是一对,所以您隐含要求 f x y 产生一对:
>>> :t myZipWith
myZipWith :: (t2 -> t3 -> (t2, t3)) -> t -> t1 -> [(t2, t3)]
解决方案是不递归,而是直接考虑每一对。
你可以use behzad.nouri's solution of enabling the ParallelListComp language extension:
>>> :set -XParallelListComp
>>> let myZipWith f xs ys = [ f x y | x <- xs | y <- ys ]
>>> myZipWith (+) [1,2,4] [0,10,20]
[1,12,24]
ParallelListComp 使列表理解中的第二个(及以后的)竖线字符 (|) 成为合法语法,与之前的列表并行(类似 zip)遍历这些列表。
很高兴知道这与普通列表推导有何不同,在普通列表推导中,您使用逗号分隔从中提取的每个列表。使用逗号进行嵌套迭代,在结果列表中被展平:
>>> let notZipWith f xs ys = [ f x y | x <- xs, y <- ys ]
>>> notZipWith (+) [1,2,4] [0,10,20]
[1,11,21,2,12,22,4,14,24]
Using the ParallelListComp extension is really just syntatical sugar for the original zipWith,所以你可能会认为它作弊。
你也可以只依赖原来的zip:
>>> let myZipWith f xs ys = [ f x y | (x,y) <- zip xs ys ]
>>> myZipWith (+) [1,2,4] [0,10,20]
[1,12,24]
但既然zip被定义为zipWith (,),那也可能是作弊。
另一种方法是使用索引:
>>> let myZipWith f xs ys = [ f x y | i <- [0..min (length xs) (length ys) - 1], let x = xs !! i, let y = ys !! i ]
>>> myZipWith (+) [1,2,4] [0,10,20]
[1,12,24]
但这将非常低效,因为!! 是线性时间运算,使得myZipWith 是二次的,而zipWith 是线性的:
>>> :set +s
>>> last $ myZipWith (+) (replicate 10000000 1) (replicate 10000000 2)
3
(4.80 secs, 3282337752 bytes)
>>> last $ zipWith (+) (replicate 10000000 1) (replicate 10000000 2)
3
(0.40 secs, 2161935928 bytes)
我确信还有其他不好的方法可以通过列表理解创建与 zipWith 等效的方法,但我并不十分相信有一种好的方法,即使是上面的方法。