Is there an easy way to translate code with threads to code using iterators? Or is there an easy way to make my first attempt more memory efficient?
@Wess Ness gave you an improved answer using Streams and gave the reasons why your code takes up as much memory and time as when adding threads before and a linear structure to the left, but no one answered completely to the second (or possibly the main) part of your the question of whether a true incremental Sieve of Eratosthenes can be realized using an Iterator.
Firstly, we should properly treat this algorithm with a right slope, from which your first code is a rough (left) example (since it prematurely adds all simple compound streams to merge operations), which is connected with Richard Byrd as in Melissaβs epilogue E. O'Neill is the final article on the incremental sieve of Eratosthenes .
Secondly, no, in this algorithm it is impossible to replace the Iterator for Stream, since it depends on moving along the stream without restarting the stream, and although you can access the iterator head (current position) using the following value (skipping overhead) to generate the rest of the iteration, since the stream requires the creation of a completely new iterator with an awful cost in memory and time. However, we can use Iterator to output the results of a sequence of primes to minimize memory usage and simplify the use of higher order iterator functions, as you will see in my code below.
Now Ness is waiting for you, although the principles of deferring the addition of simple composite threads to calculations until they are needed, which works well when you store them in a structure such as Priority Queue or HashMap, and even missed it by O'Neill, but for Richard Bird's algorithm, this is not necessary, since the future values ββof the stream will not be available until needed, so they will not be saved if the threads are created with lazy construction (both lazy and left-handed). In fact, this algorithm does not even need to memorize and the overhead of the full stream, because each sequence of rejecting a composite number moves forward without reference to any past primes, except for one, a separate source of basic primes is required, which can be provided by a recursive call of the same algorithm .
For help, list Richard Bird's Haskell code as follows:
primes = 2:([3..] 'minus' composites) where composites = union [multiples p | p <β primes] multiples n = map (n*) [n..] (x:xs) 'minus' (y:ys) | x < y = x:(xs 'minus' (y:ys)) | x == y = xs 'minus' ys | x > y = (x:xs) 'minus' ys union = foldr merge [] where merge (x:xs) ys = x:merge' xs ys merge' (x:xs) (y:ys) | x < y = x:merge' xs (y:ys) | x == y = x:merge' xs ys | x > y = y:merge' (x:xs) ys
In the following code, I simplified the minus function (called the minus StrtAt), since we do not need to create a completely new stream, but may include a compound subtraction operation with the generation of the original (in my case, only coefficients). I also simplified the function "union" (renamed it as "mrgMltpls")
Stream operations are implemented as non-memoizing generic Co Inductive Stream (CIS) as a general class, where the first field of the class is the value of the current position of the stream and the second is thunk (a null argument that returns the next value of the stream through the built-in closing arguments of another function).
def primes(): Iterator[Long] = { // generic class as a Co Inductive Stream element class CIS[A](val v: A, val cont: () => CIS[A]) def mltpls(p: Long): CIS[Long] = { var px2 = p * 2 def nxtmltpl(cmpst: Long): CIS[Long] = new CIS(cmpst, () => nxtmltpl(cmpst + px2)) nxtmltpl(p * p) } def allMltpls(mps: CIS[Long]): CIS[CIS[Long]] = new CIS(mltpls(mps.v), () => allMltpls(mps.cont())) def merge(a: CIS[Long], b: CIS[Long]): CIS[Long] = if (av < bv) new CIS(av, () => merge(a.cont(), b)) else if (av > bv) new CIS(bv, () => merge(a, b.cont())) else new CIS(bv, () => merge(a.cont(), b.cont())) def mrgMltpls(mlps: CIS[CIS[Long]]): CIS[Long] = new CIS(mlps.vv, () => merge(mlps.v.cont(), mrgMltpls(mlps.cont()))) def minusStrtAt(n: Long, cmpsts: CIS[Long]): CIS[Long] = if (n < cmpsts.v) new CIS(n, () => minusStrtAt(n + 2, cmpsts)) else minusStrtAt(n + 2, cmpsts.cont()) // the following are recursive, where cmpsts uses oddPrms and // oddPrms uses a delayed version of cmpsts in order to avoid a race // as oddPrms will already have a first value when cmpsts is called to generate the second def cmpsts(): CIS[Long] = mrgMltpls(allMltpls(oddPrms())) def oddPrms(): CIS[Long] = new CIS(3, () => minusStrtAt(5L, cmpsts())) Iterator.iterate(new CIS(2L, () => oddPrms())) {(cis: CIS[Long]) => cis.cont()} .map {(cis: CIS[Long]) => cis.v} }
The above code generates the 100,000th right (1299709) on ideone in about 1.3 seconds with about 0.36 seconds overhead and has empirical computational complexity of up to 600,000 primes around 1.43. The memory usage is slightly higher than that used by the program code.
The above code can be implemented using Scala's built-in threads, but there is an overhead of performance and memory (with a constant coefficient) that this algorithm does not require. Using Streams would mean that you could use them directly without additional Iterator generation code, but since this is only used for the final output of the sequence, it is not very expensive.
To implement basic tree addition, as Will Ness suggested, you need to add the "steam" function and connect it to the "mrgMltpls" function:
def primes(): Iterator[Long] = { // generic class as a Co Inductive Stream element class CIS[A](val v: A, val cont: () => CIS[A]) def mltpls(p: Long): CIS[Long] = { var px2 = p * 2 def nxtmltpl(cmpst: Long): CIS[Long] = new CIS(cmpst, () => nxtmltpl(cmpst + px2)) nxtmltpl(p * p) } def allMltpls(mps: CIS[Long]): CIS[CIS[Long]] = new CIS(mltpls(mps.v), () => allMltpls(mps.cont())) def merge(a: CIS[Long], b: CIS[Long]): CIS[Long] = if (av < bv) new CIS(av, () => merge(a.cont(), b)) else if (av > bv) new CIS(bv, () => merge(a, b.cont())) else new CIS(bv, () => merge(a.cont(), b.cont())) def pairs(mltplss: CIS[CIS[Long]]): CIS[CIS[Long]] = { val tl = mltplss.cont() new CIS(merge(mltplss.v, tl.v), () => pairs(tl.cont())) } def mrgMltpls(mlps: CIS[CIS[Long]]): CIS[Long] = new CIS(mlps.vv, () => merge(mlps.v.cont(), mrgMltpls(pairs(mlps.cont())))) def minusStrtAt(n: Long, cmpsts: CIS[Long]): CIS[Long] = if (n < cmpsts.v) new CIS(n, () => minusStrtAt(n + 2, cmpsts)) else minusStrtAt(n + 2, cmpsts.cont()) // the following are recursive, where cmpsts uses oddPrms and // oddPrms uses a delayed version of cmpsts in order to avoid a race // as oddPrms will already have a first value when cmpsts is called to generate the second def cmpsts(): CIS[Long] = mrgMltpls(allMltpls(oddPrms())) def oddPrms(): CIS[Long] = new CIS(3, () => minusStrtAt(5L, cmpsts())) Iterator.iterate(new CIS(2L, () => oddPrms())) {(cis: CIS[Long]) => cis.cont()} .map {(cis: CIS[Long]) => cis.v} }
The above code generates the 100,000th right (1299709) on ideone in about 0.75 seconds with about 0.37 seconds overhead and has empirical computational complexity to the 1000,000th first (15485863) about 1.09 (5.13 seconds ) The memory usage is slightly higher than that used by the program code.
Please note that the codes above are fully functional in the sense that no mutable state is used, but the Bird algorithm (or even folding the tree) is not as fast as using the priority queue or HashMap for large ranges, since the number of operations to process tree merging has higher computational complexity than the overhead of log n in the priority queue or the linear (amortized) performance of the HashMap (although there is a large overhead for hashing processing I am a constant factor, so the advantage is really not visible until some really large ranges are used).
The reason these codes use so little memory is because CIS streams are formulated without a constant reference to the beginning of the streams, so the streams are garbage collected because they are used, leaving only the minimum number of basic simple components that, as Will Ness explained, very small - only 546 basic simple composite numeric streams to generate the first million primes up to 15485863, each placeholder takes only 10 bytes (eight for a long number, eight for a 64-bit function link with two more eight bytes for a pointer to the closure arguments, and a few more bytes for the service messages of functions and classes for a common placeholder for a stream of 40 bytes or a total of not more than 20 kilobytes to generate a sequence for a million primes).