do-notation/enumFromTo 中的模式匹配会降低 Haskell 代码的速度吗?
Could pattern-matching in do-notation/enumFromTo slow down a Haskell code?
我一直在解决一个非常简单的问题:生成所有长度为 L
的递减序列,由按字典顺序从 1
到 M
的自然数组成。
然而,我 运行 遇到了一个 相当 st运行ge 的问题。看看:
c :: (Ord a, Num a, Enum a) => a -> a -> [[a]]
c m 1 = map return [1..m]
c m l = do
n <- [l..m]
res <- c (n - 1) (l - 1)
return $ n:res
c' :: (Ord a, Num a, Enum a) => a -> a -> [[a]]
c' m = helper 1 m where
helper :: (Ord a, Num a, Enum a) => a -> a -> a -> [[a]]
helper a b 1 = map return [a..b]
helper a b l = do
n <- [a..b]
True <- return $ (l - 1 <= n)
res <- helper a (n - 1) (l - 1)
return (n:res)
所以,很明显,这两个函数做的事情完全相同(我在许多测试中检查了它们,它们都给出了正确的结果),但是如果你尝试评估 c 100 98
和 c' 100 98
在 GHCi 中,你会发现它所花费的时间有很大的不同:
c 100 98: around 5 seconds;
c' 100 98: around 70 seconds;
As I've mentioned, the result is the same.
所以,我对每次生成 [a..b]
感到有点不安,但我做了一些询问,有人建议 Haskell 模式匹配不正确马上就可以了,但是由于惰性求值而延迟了它,这会导致 c'
的大量额外调用。然而,第二种理论并不完全成立:我直接从 GHCi 命令提示符在我的代码中设置了一个断点,以监视 n
的值,这表明延迟模式匹配不是这种情况。
问题可能出在 enumFromTo
函数上,还是有其他原因?
这两个函数的实现好像完全不同:
c m l = do
n <- [l..m]
res <- c (n - 1) (l - 1)
return $ n:res
此处,在每次递归调用时,参数 l
递减,而参数 m
变为 n <- [l--m]
。
相比之下,
helper a b l = do
n <- [a..b]
True <- return $ (l - 1 <= n)
res <- helper a (n - 1) (l - 1)
return (n:res)
这里的间隔是 [a..b]
而不是 [l..m]
(顺便说一句,为什么你使用不同的名称?这样比较两个片段更难。)所以,我们考虑如何参数 a
和 b
改变。参数a
不变,而b
变为n-1
.
还有第三个参数 l
,第一个片段中没有。
我不明白这怎么会是同一个算法。在我看来完全不同。您可能在这里引起了更多的递归调用,这会减慢速度。模式匹配是一个转移注意力的问题——我认为这不会减慢速度,至少不会直接减慢速度。
还有这部分
n <- [a..b]
True <- return $ (l - 1 <= n)
看起来很可疑。应该是这样的
n <- [max a (l-1) .. b]
因为上面将从 a
计数到 l-2
只是为了丢弃下一行中的那些选择。仅生成选择以丢弃它们会减慢您的程序。
将您的 True <- return $ (l - 1 <= n)
更改为 True <- return $ (l <= n)
,以匹配第一个代码段所做的事情,这对我来说平衡了两者的时间(不改变答案)。
如果不进行此更改,您的第二个代码段将浪费大量时间来尝试在数字 [1..l-1]
中查找长度 l
的递减序列(对于 l
的许多不同值),注定的任务。
我一直在解决一个非常简单的问题:生成所有长度为 L
的递减序列,由按字典顺序从 1
到 M
的自然数组成。
然而,我 运行 遇到了一个 相当 st运行ge 的问题。看看:
c :: (Ord a, Num a, Enum a) => a -> a -> [[a]]
c m 1 = map return [1..m]
c m l = do
n <- [l..m]
res <- c (n - 1) (l - 1)
return $ n:res
c' :: (Ord a, Num a, Enum a) => a -> a -> [[a]]
c' m = helper 1 m where
helper :: (Ord a, Num a, Enum a) => a -> a -> a -> [[a]]
helper a b 1 = map return [a..b]
helper a b l = do
n <- [a..b]
True <- return $ (l - 1 <= n)
res <- helper a (n - 1) (l - 1)
return (n:res)
所以,很明显,这两个函数做的事情完全相同(我在许多测试中检查了它们,它们都给出了正确的结果),但是如果你尝试评估 c 100 98
和 c' 100 98
在 GHCi 中,你会发现它所花费的时间有很大的不同:
c 100 98: around 5 seconds;
c' 100 98: around 70 seconds;
As I've mentioned, the result is the same.
所以,我对每次生成 [a..b]
感到有点不安,但我做了一些询问,有人建议 Haskell 模式匹配不正确马上就可以了,但是由于惰性求值而延迟了它,这会导致 c'
的大量额外调用。然而,第二种理论并不完全成立:我直接从 GHCi 命令提示符在我的代码中设置了一个断点,以监视 n
的值,这表明延迟模式匹配不是这种情况。
问题可能出在 enumFromTo
函数上,还是有其他原因?
这两个函数的实现好像完全不同:
c m l = do
n <- [l..m]
res <- c (n - 1) (l - 1)
return $ n:res
此处,在每次递归调用时,参数 l
递减,而参数 m
变为 n <- [l--m]
。
相比之下,
helper a b l = do
n <- [a..b]
True <- return $ (l - 1 <= n)
res <- helper a (n - 1) (l - 1)
return (n:res)
这里的间隔是 [a..b]
而不是 [l..m]
(顺便说一句,为什么你使用不同的名称?这样比较两个片段更难。)所以,我们考虑如何参数 a
和 b
改变。参数a
不变,而b
变为n-1
.
还有第三个参数 l
,第一个片段中没有。
我不明白这怎么会是同一个算法。在我看来完全不同。您可能在这里引起了更多的递归调用,这会减慢速度。模式匹配是一个转移注意力的问题——我认为这不会减慢速度,至少不会直接减慢速度。
还有这部分
n <- [a..b]
True <- return $ (l - 1 <= n)
看起来很可疑。应该是这样的
n <- [max a (l-1) .. b]
因为上面将从 a
计数到 l-2
只是为了丢弃下一行中的那些选择。仅生成选择以丢弃它们会减慢您的程序。
将您的 True <- return $ (l - 1 <= n)
更改为 True <- return $ (l <= n)
,以匹配第一个代码段所做的事情,这对我来说平衡了两者的时间(不改变答案)。
如果不进行此更改,您的第二个代码段将浪费大量时间来尝试在数字 [1..l-1]
中查找长度 l
的递减序列(对于 l
的许多不同值),注定的任务。