4

I've been playing around with Haskell and find it fascinating, especially the Lazy Evaluation feature, which allows us to work with (potentially) infinite lists.

From this, derives the beautiful implementation of the Sieve of Eratosthenes to get an infinite list of primes:

primes = sieve [2..]
  where 
  sieve (x:xs) = x : sieve [i | i <- xs, i `mod` x /= 0]

Still using Haskell, I can have either:

takeWhile (<1000) primes

which gives me the primes until 1000 (n), or

take 1000 primes

which gives me the first 1000 primes


I tried to implement this in Javascript, forgetting the 'infinite' possibility and this is what i came up with:

const sieve = list => {
  if (list.length === 0) return []
  const first = list.shift()
  const filtered = list.filter(x => x % first !== 0)
  return [first, ...sieve(filtered)]
}

const getPrimes = n => {
  const list = new Array(n - 1).fill(null).map((x, i) => i + 2)
  return sieve(list)
}

It works perfectly (if I don't reach maximum call stack size first), but I can only get the prime numbers "up until" n.

How could i use this to implement a function that would instead return "the first n" primes?

I've tried many approaches and couldn't get it to work.


Bonus

is there any way I can use tail call optimization or something else to avoid Stack Overflows for large Ns?

Will Ness
  • 70,110
  • 9
  • 98
  • 181
André Alçada Padez
  • 10,987
  • 24
  • 67
  • 120
  • 2
    Probably worth just converting this into a generator. Then you can have a very generic `take` and `takeUntil` iterable helpers that will work with your prime generator. – VLAZ Nov 05 '21 at 22:28
  • @VLAZ i like where your mind is, but i don't see how that would work with the sieve algorithm. If you can make it work, could you expand it in the form of an answer? would be greatly appreciated – André Alçada Padez Nov 05 '21 at 22:31
  • 1
    I'd give it a go tomorrow, unless somebody tries it before that. It's about bed time now. But here are the steps: keep a list of primes starting with `2`. For each following number number, check if it's multiple of a prime and discard it if it is (Erathostenes), or yield and keep it if it isn't. Alternatively, you can generate all multiples of a prime (up to some limit) and keep them in a Set. Then check `nonPrimes.has(i)` to decide. With that `take` and `takeUntil` are easy - first one takes an iterable and `n` and yields `n` values, the other an iterable and a predicate and checks and yields – VLAZ Nov 05 '21 at 22:38
  • 1
    Remember not to fall into the trap of "running recursive code without caching the results". Whether it's the fibonacci series or the sieve of eratosthenes, maintain a global list of values-already-found, so you don't rerun the computations over and over and over and over and [...] - consult your LUT to see "up to where" you already know the result, and directly use that, instead of recursing. – Mike 'Pomax' Kamermans Nov 05 '21 at 22:49
  • @Mike'Pomax'Kamermans that's definitely a good point, but wouldn't work with Sieve; sieve [7..10] would never work – André Alçada Padez Nov 05 '21 at 22:53
  • Just out of curiosity, did you look at the pseudocode on https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes#Pseudocode? (relatively easily adapted to JS, although you definitely want to lean on `Array.filter` at the end) – Mike 'Pomax' Kamermans Nov 05 '21 at 22:59
  • @Mike'Pomax'Kamermans, i hadn't, but ... "This algorithm produces all primes not greater than n. " which is exactly what my js code is doing... doesn't cover what i'm looking for. By the way, thanks for the edits – André Alçada Padez Nov 05 '21 at 23:01
  • @Mike'Pomax'Kamermans, nevertheless, the following sections: "segmented sieve" and "incremental sieve" may hold the answer to my question, but i'll have to look into it tomorrow as i'm almost brain dead right now – André Alçada Padez Nov 05 '21 at 23:04
  • 1
    ah, sorry, you want `n` primes, not all primes below and up to `n`. Honestly... start at a higher `n` =D (thanks to Gauss, who gave us `π(x)`) – Mike 'Pomax' Kamermans Nov 05 '21 at 23:09
  • @Mike'Pomax'Kamermans: yes. my getPrimes function is already doing "up to n" – André Alçada Padez Nov 05 '21 at 23:11
  • 1
    (noting that the sieving approach technically, and we're talking maths so that matters, does not calculate "n primes" the sieve of erestothenes. Exploiting π(x)=n as a "prestep" to sieving makes mathematical sense) – Mike 'Pomax' Kamermans Nov 05 '21 at 23:16
  • everybody, just posted my own solution. Any feedback and possible improvements would be welcome – André Alçada Padez Nov 07 '21 at 20:57
  • Search my answers for "postponed sieve" to see the technique how you can achieve quadratic Improvement in both execution speed and the reach of your generated progression of primes as regards the stack overflow errors. – Will Ness Mar 19 '23 at 17:02
  • here it means that you can stop sieving your list when `first`'s **square** becomes larger than the last value in the list -- by this point all the values in the list are already only primes, and no non-primes are present in it. This way you will be able to process much much longer lists before you encounter the stack overflow error, than now. – Will Ness Mar 21 '23 at 01:28

3 Answers3

2

As @VLAZ suggested, we can do this using generators:

function* removeMultiplesOf(x, iterator) {
  for (const i of iterator)
    if (i % x != 0)
      yield i;
}
function* eratosthenes(iterator) {
  const x = iterator.next().value;
  yield x;
  yield* eratosthenes(removeMultiplesOf(x, iterator));
}
function* from(i) {
  while (true)
    yield i++;
}
function* take(n, iterator) {
  if (n <= 0) return;
  for (const x of iterator) {
    yield x;
    if (--n == 0) break;
  }
}

const primes = eratosthenes(from(2));
console.log(Array.from(take(1000, primes)));

Btw, I thought one might be able to optimise this by not doing division repeatedly:

function* removeMultiplesOf(x, iterator) {
  let n = x;
  for (const i of iterator) {
    while (n < i)
      n += x;
    if (n != i)
      yield i;
  }
}

but a quick benchmark showed it actually is about as fast as the simple function.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
0

Alright, after working all weekend on this, i think i found my best implementation.

My solution uses proper caching (using the power of closures) of previous results so, the performance keeps getting better the more you use it

To get the first N primes, I iterate through the getPrimesTill until i reach a sufficient length... there is a compromise here, which will find more primes than intended on the first time but i don't think it can be any other way. Maybe getPrimesTill(n + ++count * n * 5) can be further optimized but i think this is more than good enough.

To be able to handle very large numbers while avoiding stack overflows, i implemented the sieve algorithm using a for loop, instead of recursion.

Here's the code:

function Primes() {
  let thePrimes = []

  const shortCircuitPrimes = until => {
    const primesUntil = []
    for (let i = 0; ; i++) {
      if (thePrimes[i] > until) {
        return primesUntil
      }
      primesUntil.push(thePrimes[i])
    }
  }

  const sieveLoop = n => {
    const list = buildListFromLastPrime(n)
    const result = []
    let copy = [...thePrimes, ...list]
    for (let i = 0; i < result.length; i++) {
      copy = copy.filter(x => x % result[i] !== 0)
    }
    for (let i = 0; ; i++) {
      const first = copy.shift()
      if (!first) return result
      result.push(first)
      copy = copy.filter(x => x % first !== 0)
    }
  }

  const buildListFromLastPrime = n => {
    const tpl = thePrimes.length
    const lastPrime = thePrimes[tpl - 1]
    const len = n - (lastPrime ? tpl + 1 : 1)
    return new Array(len).fill(null).map((x, i) => i + 2 + tpl)
  }

  const getPrimesTill = n => {
    const tpl = thePrimes.length
    const lastPrime = thePrimes[tpl - 1]
    if (lastPrime > n) {
      return shortCircuitPrimes(n)
    }

    const primes = sieveLoop(n)
    if (primes.length - thePrimes.length) {
      thePrimes = primes
    }
    return primes
  }

  const getFirstPrimes = n => {
    let count = 0
    do {
      if (thePrimes.length >= n) {
        return thePrimes.slice(0, n)
      }
      getPrimesTill(n + ++count * n * 5)
    } while (true)
  }

  return { getPrimesTill, getFirstPrimes, thePrimes }
}

const { getPrimesTill, getFirstPrimes, thePrimes } = Primes()

I created a repo for it, with exhaustive testing anyone wants to give it a go.

https://github.com/andrepadez/prime-numbers-sieve-eratosthenes-javascript

The entire test suite takes about 85s to run, as i'm testing with many possible combinations and very large numbers.
Also, all the expected results were obtained from the Haskell implementation, so to not to pollute the tests.


Also, I found this awesome video, where the guy implements Lazy Evaluation and Infinite Lists using TypeScript... In the end, he builds the Sieve Algorithm in Javascript, working exactly like intended in Haskell

https://www.youtube.com/watch?v=E5yAoMaVCp0

André Alçada Padez
  • 10,987
  • 24
  • 67
  • 120
0

All those generators have been available for a while within prime-lib:

  • Get the first 10 primes:
import {generatePrimes, stopOnCount} from 'prime-lib';

const i = generatePrimes(); // infinite primes generator

const k = stopOnCount(i, 10); // the first 10 primes

console.log(...k); //=> 2 3 5 7 11 13 17 19 23 29
  • Get all primes between 1000 and 1100:
import {generatePrimes, stopOnValue} from 'prime-lib';

const i = generatePrimes({start: 1000});

const k = stopOnValue(i, 1100);

console.log(...k); //=> 1009 1013 ... 1093 1097

...etc., plenty of examples within the library itself.

P.S. I am the author.

vitaly-t
  • 24,279
  • 15
  • 116
  • 138