在一次编程练习中,首先要求对阶乘函数进行编程,然后计算总和:1! 2! 3!... n!
在O(n)
乘法中(所以我们不能直接使用阶乘)。我不是在寻找这个特定(琐碎)问题的解决方案,我试图探索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_fact n = go 1 1 1
where go acc fact i
| i <= n = go (acc + fact) (fact * i) (i + 1)
| otherwise = acc
事实上,这是“尾递归”(调用go
不会将对go
的任何子调用管道到另一个函数,如()
或(*)
),这意味着编译器可以将其打包到一个非常紧密的循环中,这就是为什么我将其与“for loops”进行比较,尽管这并不是Haskell的原生想法。
上面的sum_factn=sum(接受n个事实)
比这个慢一点,但比sum(接受n个事实)
快一点,其中事实
是用zipAnd
定义的。速度差异不是很大,我认为主要是由于不再使用的内存分配。
您的示例在内存使用方面并不相同。很容易看出,您是否将*
替换为一个>(这样数字就不会太快变大),然后在一个大的
n
上运行这两个示例,例如10^7。Haskell版本将消耗大量内存,python将保持较低的内存。
Python生成器不会生成一个值列表,然后对其求和。相反,sum函数将从生成器中逐个获取值并进行累加。因此,内存使用将保持不变。
Haskell将懒散地计算函数,但为了计算say
。有关评估和减少的更多详细信息,请参见:https://www.haskell.org/haskellwiki/Foldr_Foldl_Foldl'.foldl1()(以事实为例)
它必须计算完整的表达式。对于大型表达式,这将以与大型表达式相同的方式展开(foldl()0[0..n])
您可以使用
foldl1'
而不是上面链接中描述的foldl1
来修复您的sum n
。正如@user2407038在他的评论中解释的那样,您还需要将事实
保持在本地。以下操作在GHC中使用恒定内存:
let notfact = scanl1 (+) [1..]
let n = 20000000
let res = foldl' (+) 0 (take n notfact)
请注意,如果用实际阶乘代替
Not事实
,内存考虑就不那么重要了。数字会很快变大,任意精度的算术会减慢速度,所以你无法得到n
的大值来实际看到差异。
实际上,懒惰列表可以这样使用。但也有一些细微的区别:
但是Haskell提供了更高级的工具来完成生成器/消费者模式。目前有三个库专注于这个问题:管道、管道和迭代器。我最喜欢的是管道,它易于使用,并且其类型的复杂性保持在较低水平。
它们有几个优点,特别是您可以创建复杂的管道,并且可以将它们基于选定的monad,这允许您说出管道中允许的副作用。
使用管道,您的示例可以表达如下:
import Data.Functor.Identity
import Data.Conduit
import qualified Data.Conduit.List as C
ifactC :: (Num a, Monad m) => Producer m a
ifactC = loop 1 1
where
loop r n = let r' = r * n
in yield r' >> loop r' (n + 1)
sumC :: (Num a, Monad m) => Consumer a m a
sumC = C.fold (+) 0
main :: IO ()
main = (print . runIdentity) (ifactC $= C.isolate 5 $$ sumC)
-- alternatively running the pipeline in IO monad directly:
-- main = (ifactC $= C.isolate 5 $$ sumC) >>= print
在这里,我们创建一个无限期产生阶乘的生产者(一个不消耗任何输入的管道)。然后我们用隔离来组合它,这确保通过它传播的值不超过给定数量,然后我们用消费者来组合它,消费者只对值求和并返回结果。
本文向大家介绍Haskell优雅的镜片,包括了Haskell优雅的镜片的使用技巧和注意事项,需要的朋友参考一下 示例 除了makeLenses用于生成Lenses的标准功能外,Control.Lens.TH还提供该makeClassy功能。makeClassy具有相同的类型,并以与基本上相同的方式工作makeLenses,但有一个关键区别。除了生成标准的镜头和遍历之外,如果该类型没有参数,它还将创
可以以与斐波那契数列类似的方式生成如下, 如何定义阶乘级数。 使现代化 令人尴尬的是,在添加这个问题之前,我已经试过了, 事实上,尾部的数字并不是连续的值。
问题内容: 我在Linux上编写了一个C程序,该程序可以分配内存,并在一个循环中运行它,而TOP没有显示任何内存消耗。 然后我对该内存做了一些操作,而TOP确实显示了内存消耗。 当我分配内存时,我真的是“获取内存”,还是有一个“惰性”内存管理,仅当使用时才给我内存? (还有一个选项,当我使用它时,TOP只知道内存消耗,因此我不确定。) 谢谢 问题答案: 在Linux上,malloc使用sbrk()
本文向大家介绍Python进阶:生成器 懒人版本的迭代器详解,包括了Python进阶:生成器 懒人版本的迭代器详解的使用技巧和注意事项,需要的朋友参考一下 从容器、可迭代对象谈起 所有的容器都是可迭代的(iterable),迭代器提供了一个next方法。iter()返回一个迭代器,通过next()函数可以实现遍历。 除了数字外,其他数据结构都是可迭代的。 生成器是什么 生成器是懒人版本的迭代器。例
问题内容: 我想创建自己的集合,该集合具有python list的所有属性,并且还知道如何将自身保存到数据库中或从数据库中加载。我也想使负载隐式和惰性,因为在列表创建时它不会发生,而是等到第一次使用时才发生。 有没有一种单一的方法,我可以覆盖上加载任何列表属性(如第一次使用清单,,而不必重写他们… …等)? 问题答案: 不,没有。
问题内容: 考虑以下代码: 当第一个URL够用时会被要求输入第二个URL吗? 我尝试了一个较小的示例,它看起来像预期的那样工作。即一个一个地处理数据,但是可以依靠这种行为吗?如果没有,在帮助之前打电话吗? 输出: 更新 :如果对实施很重要,请使用官方Oracle JDK8 答案 :根据下面的评论和答案,flatmap部分是惰性的。即完全读取第一个流,并且仅在需要时才读取下一个。渴望读取一个流,但是