-
Notifications
You must be signed in to change notification settings - Fork 790
Description
In the help notes for this "sieve" block included in the demos for the Streams library, you say "It's called SIEVE because the algorithm it uses is the Sieve of
Eratosthenes: https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes
", yet when one reads the linked Wikipedia article (at least as updated over the last 15 years or so), one finds the following statement: "The widely known 1975 functional sieve code by David Turner[13] is often presented as an example of the sieve of Eratosthenes[7] but is actually a sub-optimal trial division sieve.[2]".
That David Turner sieve is exactly what you have implemented, as expressed in Haskell as follows:
primes = sieve [2..]
sieve (p : xs) = p : sieve [x | x <− xs, x `mod` p > 0]or more as you have written it in Snap! (using Haskell's "filter" instead of the equivalent Snap! "keep"):
primes = sieve [2..]
sieve (p : xs) = p : sieve (filter (\ c -> c `mod` p > 0) xs)This has much worse performance than a real functional "incremental" Sieve of Eratosthenes (SoE) so as to be almost unusable for your Snap! version, taking over 10 seconds on a fairly high end desktop computer to find the primes just to a thousand; it is slow because rather than using SoE sieving by only incrementing the culled composites per base prime by addition of a constant span, it uses trial division (via the "mod" function) to test ALL remaining prime candidates for even division by all found primes - thus, it has an exponential computational complexity rather than the O(n log n (log log n)) computational complexity for the true incremental SoE.
Using the work from the Wikipedia article referenced article, one could write a true incremental SoE using a Priority Queue as O'Neill preferred but that would require the extra work of implementing the Priority Queue using either a binary tree structure or using Snap!'s List's in imperative mode (not functional linked-list mode). In the Epilogue of the article, reference is made to a "Richard Bird list-based version" of the SoE, which is much easier to implement. However, at that time, this sieve still hadn't been optimized to use "infinite tree folding" to reduce the complexity of the base primes composites merging to a computational complexity of about O(n log n) rather than O(n sqrt n) without the tree folding, which is a very significant difference as prime ranges get larger. A refined version of this sieve (sieves odds-only) in Haskell is as follows:
primes = 2 : oddprimes() where
merge xs@(x : xtl) ys@(y : ytl) =
case compare x y of
EQ -> x : merge xtl ytl
GT -> y : merge xs ytl
LT -> x : merge xtl ys
composites ((hd : tl) : rest) = hd : merge tl (composites $ pairs rest) where
pairs (f : s : rest) = merge f s : pairs rest
testprmfrm n cs@(c : rest) =
if n >= c then testprmfrm (n + 2) rest else n : testprmfrm (n + 2) cs
oddprimes() = (3 :) $ testprmfrm 5 $ composites $ map (\ bp -> [bp * bp, bp * bp + bp + bp .. ]) $ oddprimes()In contrast to the David Turner sieve, instead of progressively sieving composite numbers from the input list by filtering the entire remaining list, this tree folding algorithm rather builds a merged (and therefore ascending ordered) lazy sequence of all of the composite values based on the recursively determined "oddprimes" function (non-sharing) and then the final output sequence of prime numbers is all of the (in this case odd) numbers that aren't in the composites sequence. It is relatively easy to arrange the merged sequence of composites in ascending order because each of the sub lists of culls from each of the base primes is in order but also because each new sub sequence based on the next base prime will start at a new increased value from the last (starting at the square of the base prime). For this fully functional "incremental" algorithm, there is a cost in merging for all of the sub sequences, but the binary tree depth is limited: for instance, when finding the primes up to a million, the base primes sequence only goes up to the square root or a thousand, and there are only 167 odd primes up to a thousand, meaning that the binary tree only has a depth of about eight (two to the power of eight is 256) in the worst case (some merges won't need to traverse to the bottom of the tree)
Now, as all of these sequence states don't refer back to previous states, there is no need to memoize the states as in a "lazy list" or as in your "streams" library, and a simpler Co-Inductive Stream (CIS) can be used, as follows in Haskell:
data CIS a = CIS !a !(() -> CIS a)
primes = CIS 2 $! oddprimes where
merge xs@(CIS x xtl) ys@(CIS y ytl) =
case compare x y of
EQ -> CIS x $! \ () -> merge (xtl()) (ytl())
GT -> CIS y $! \ () -> merge xs (ytl())
LT -> CIS x $! \ () -> merge (xtl()) ys
composites (CIS (CIS hd tl) rest) = CIS hd $! \ () -> merge (tl()) (composites $! pairs (rest())) where
pairs (CIS f ftl) = let (CIS s rest) = ftl() in CIS (merge f s) $! \ () -> pairs (rest())
allmults (CIS bp bptl) = CIS (cullfrmstp (bp * bp) (bp + bp)) $! \ () -> allmults (bptl()) where
cullfrmstp c adv = CIS c $! \ () -> cullfrmstp (c + adv) adv
testprmfrm n cs@(CIS c rest) =
if n >= c then testprmfrm (n + 2) (rest())
else CIS n $! \ () -> testprmfrm (n + 2) cs
oddprimes() = CIS 3 $! \ () -> testprmfrm 5 $! composites $! allmults $! oddprimes()The above Haskell code is written so as to better understand as this "CIS" implementation is written in non-lazy languages as it doesn't take advantage of Haskell's built-in laziness and bypasses the compiler "strictness analyzer" so may be less efficient that the lazy list version above in spite of not doing the extra work to do memoization of each sequence value...
The above has been translated to Snap! as per this attached file and can be tested with "listncis 168 primes" to show the list of the primes up to a thousand in about a second, and works in a little over ten seconds showing primes up to about ten thousand; it thus has more of a linear execution time with increasing ranges...
I recommend you fix your Streams demo to use an implementation of this algorithm, although as noted above, the algorithm doesn't really required the overhead of the Streams memoization...