是哈斯克尔';懒惰是Python的优雅替代品';什么是发电机?
在编程练习中,首先要求它编程阶乘函数,然后计算和:是哈斯克尔';懒惰是Python的优雅替代品';什么是发电机?,python,haskell,lazy-evaluation,Python,Haskell,Lazy Evaluation,在编程练习中,首先要求它编程阶乘函数,然后计算和:1!+2! + 3! +... nin(因此我们不能直接使用阶乘)。我不是在寻找这个具体(琐碎)问题的解决方案,我在尝试探索Haskell的能力,这个问题是我想玩的玩具 我认为Python的生成器可以很好地解决这个问题。例如: from itertools import islice def ifact(): i , f = 1, 1 yield 1 while True: f *= i i
1!+2! + 3! +... n在O(n)
乘法中的code>in(因此我们不能直接使用阶乘)。我不是在寻找这个具体(琐碎)问题的解决方案,我在尝试探索Haskell的能力,这个问题是我想玩的玩具
我认为Python的生成器可以很好地解决这个问题。例如:
from itertools import islice
def ifact():
i , f = 1, 1
yield 1
while True:
f *= i
i += 1
yield f
def sum_fact(n):
return sum(islice(ifact(),5))
然后我试图找出Haskell是否有类似的行为,而不是这个生成器,我认为懒惰会影响所有员工,而没有任何额外的概念
例如,我们可以用
fact = scan1 (*) [1..]
然后用以下方法解决练习:
sum n = foldl1 (+) (take n fact)
我想知道这个解决方案在时间复杂度和内存使用方面是否真的与Python的解决方案“等效”。我想说,Haskell的解决方案从不存储所有列表事实,因为它们的元素只使用一次
我是对的还是完全错了
编辑:
我应该更准确地检查:
Prelude> foldl1 (+) (take 4 fact)
33
Prelude> :sprint fact
fact = 1 : 2 : 6 : 24 : _
因此(我的实现)Haskell存储结果,即使它不再使用。基本上,是的:Haskell的惰性列表非常类似于Python的生成器,如果这些生成器可以轻松地克隆、缓存和组合的话。您可以从递归函数返回[]
,它可以将状态线程化到生成器中,而不是引发StopIteration
由于自递归,它们做了一些更酷的事情。例如,您的阶乘生成器更惯用地生成,如:
facts = 1 : zipWith (*) facts [1..]
或者Fibonaccis作为:
fibs = 1 : 1 : zipWith (+) fibs (tail fibs)
通常,任何迭代循环都可以通过将循环状态提升为函数的参数,然后递归调用它来获得下一个循环周期,从而转换为递归算法。生成器就是这样的,但是我们在递归函数的每次迭代中都会预先准备一些元素,`go ___=(stuff):go ___;
因此,完美的等价物是:
ifact :: [Integer]
ifact = go 1 1
where go f i = f : go (f * i) (i + 1)
sum_fact n = sum (take n ifact)
就什么是最快而言,Haskell中绝对最快的可能是“for循环”:
sum\u事实n=go 1
我去哪里
|我你的例子在内存使用方面不相等。很容易看出您是否将*
替换为+
(这样数字就不会太快变大),然后在大型n
上运行这两个示例,例如10^7。您的Haskell版本将消耗大量内存,python将保持较低的内存
Python生成器不会生成一个值列表,然后对其求和。相反,sum
函数将从生成器中逐个获取值并进行累加。因此,内存使用将保持不变
Haskell将惰性地计算函数,但为了计算sayfoldl1(+)(以事实为例)
它必须计算完整的表达式。对于大型n
而言,这将以与(foldl(+)0[0..n])
相同的方式展开为一个大型表达式。有关评估和缩减的更多详细信息,请参见此处:
您可以使用foldl1'
而不是上面链接中描述的foldl1
来修复您的sum n
。正如@user2407038在他的评论中解释的那样,您还需要保持fact
local。以下内容在GHC中使用恒定内存:
let notfact=scanl1(+)[1..]
设n=20000000
设res=foldl'(+)0(取n个notfact)
请注意,在实际阶乘代替notfact
的情况下,内存方面的考虑就不那么重要了。数字会很快变大,任意精度的算术运算会减慢速度,因此您无法获得n
的大值以实际看到差异。确实,惰性列表可以这样使用。但也有一些细微的区别:
- 列表是数据结构。因此,您可以在评估它们之后保留它们,这可能是好的,也可能是坏的(您可以避免重新计算值和使用@ChrisDrost所描述的递归技巧,但代价是保持内存未释放)
- 清单是纯粹的。在生成器中,您可以进行带有副作用的计算,但不能使用列表(这通常是可取的)
- 由于Haskell是一种懒惰的语言,懒惰无处不在,如果您只是将一个程序从命令式语言转换为Haskell,内存需求可能会发生很大的变化(正如@RomanL在他的回答中所描述的)
但是Haskell提供了更高级的工具来完成生成器/使用者模式。目前有三个库关注此问题:。我最喜欢的是,它易于使用,并且其类型的复杂性保持较低
它们有几个优点,特别是您可以创建复杂的管道,并且可以基于选定的monad创建管道,这允许您说明管道中允许哪些副作用
使用导管,您的示例可以表示为:
import Data.Functor.Identity
导入数据。管道
将符合条件的Data.conductor.List作为C导入
IFATCC::(数字a,单子m)=>生产者m a
IFATCC=回路1
哪里
循环rn=设r'=r*n
在产量r'>>回路r'(n+1)中
sumC::(Num a,Monad m)=>消费者a m a
sumC=C.fold(+)0
main::IO()
main=(print.runIdentity)(IFATCC$=C.5$$sumC)
--或者直接在IO monad中运行管道:
--主=(IFATCC$=C.5$$sumC)>>=打印
在这里,我们创建一个无限期产生阶乘的生产者
(一个不消耗任何输入的管道)。然后我们用隔离
组合它,它确保通过它传播的值不超过给定数量,然后我们用一个消费者
组合它,它只对值求和并返回结果。有一些相似之处,但也有一些不同之处:Python ge
sum_fact n = go 1 1 1
where go acc fact i
| i <= n = go (acc + fact) (fact * i) (i + 1)
| otherwise = acc