A regular expression to check for prime numbers (2007)
noulakaz.net
noulakaz.net
The 2022 discussion has a more formal proof using the pumping lemma.
The pumping lemma is a great milestone in this field, so simple, yet so powerful.
If the "working on something else" means "working on some hard research problem", this is not embarrassing, but stylish. :-)
Sorry I can't parse that
TIL that the computer-sciencey definition of RE isn't the only one in use.
https://en.wikipedia.org/wiki/Perl_Compatible_Regular_Expres...
"Regular Expression Matching Can Be Simple And Fast" (2007)
Regular languages are also closed under intersection and taking prefix etc (see https://en.wikipedia.org/wiki/Regular_language#Closure_prope...), and if you want to expose those operators in your regular expressions, Russ Cox's technique doesn't really work.
I also suspect that this might result in a combinatorial explosion of fused terms, but that's irrelevant as we're only worried about whether intersection can be implemented in Russ Cox's method, not whether a regular expression might require several earths or so worth of RAM to hold :)
Nothing in the wiki article mentions taking a prefix, but that just sounds like `(prefix|)`?
As you point out Russ Cox's method can not deal with complement and intersection. You can hack it up by putting a compiler in front, but then you might as well compile to finite automata directly. Or you use derivatives: https://en.wikipedia.org/wiki/Brzozowski_derivative or https://www.ccs.neu.edu/home/turon/re-deriv.pdf (or https://well-typed.com/blog/2020/06/fix-ing-regular-expressi... for a Haskell flavour).
> Nothing in the wiki article mentions taking a prefix, but that just sounds like `(prefix|)`?
They implicitly mention prefixes, when they talk about 'the trio operations: string homomorphism, inverse string homomorphism, and intersection with regular languages.' If I remember right, the prefix operation is:
Take a language L, restrict it to the strings that have the right prefix: `L' := L & (prefix.*)` then chop off that prefix from every member of L'.
See https://en.wikipedia.org/wiki/Quotient_of_a_formal_language which this is a special case of. The Brzozowski derivative article also explains it in more detail.
Which is why the "RE" in the article is excrutiatingly slow, given that it needs to perform insane amounts of backtracking. In contrast, "real" regular expression checkers run in linear time.
Also, nitpicking, but they take unbounded time. It's still finite.
The inflationary use of "infinite" especially in tech crowds that should know better deserves pushback.
There are models of computation that actually model infinite computation. A well-studied one is Büchi automata, basically a generalization of finite state automata to infinite sequences.
> And I’m not sure about unbounded; surely it is bounded by a suitable exponential function?
Nope. You can nest them and for any function that you give you can produce a RE that takes longer than that function to evaluate. (That's literally what "unbounded" means. There is no bound. Doesn't mean it's "infinite". All those computations terminate, which is literally what "finite" means in this context.)
Coming up with an efficient prime-finding algorithm like Miller-Rabin* is far from trivial.
* Technically it's just a prime-checking algorithm, but you can just generate random numbers until you've verified one of them is prime.
The machine I'm using to type this comment is also a finite state machine, as due to having non-infinite memory, it also cannot count arbitrarily high.
Informally, regular expressions like in Perl are a kind of string rewriting. And many string rewriting rules, if they can be iteratively applied, are Turing-complete (with the usual caveat about infinite storage and time). The usual definition of a regular expression excludes that kind of iterative application, though. It's just a matching pattern; rewriting is technically outside of the concept of a regular expression.
Right. And if you really want to push that argument further then there are numbers for which your machine can't decide whether they are prime or not. Most of them, in fact, i.e., for all but a finite number of primes and non-primes, your machine can't tell.
For the theory of computability, this is not a useful model. Neither for complexity theory. Statements like "My machine can sort in constant time" which is arguably true but not useful either. It only holds up to a certain collection size which in a sense makes it both nonsensical and useless. Even concepts like the Chomsky hierarchy with context free languages being one of the prominent components make little sense as long as your computational model is finite state.
Instead, we model them as unbounded, to be able to differentiate between classes of computation problems as well as reason about complexity. The Turing Machine is one of the simple and elegant models, but there are others for other purposes.
I wish that basic education about the theory of computation would stress more why theory treats things like this, to avoid this kind of confusion. (And then people would need to take those classes as well, and not just claim they can write React apps just fine without a degree.)
Weird tangent. They can write React apps without a degree, can't they? What's wrong with that?
The amount of complexity reasoning you need for something like that is around "this part seems slow, maybe there's a better algorithm somewhere" or "that's a lot of nested loops, maybe I can do with less". Knowing more might help in some pretty rare cases but doesn't seem like a requirement. On the other hand, knowing the complexity of all algorithms and blindly assuming "good complexity = good performance" while never checking is a recipe for desaster.
Exactly. That's the difference between an actual education and having looked up some Big-O notation on Wikipedia.
Come on, can we drop this useless trivia that floats around? No, computers are Turing-machines for all practical purposes and no the memory is not the tape, unless your computer is sandboxed from everything. As soon as it has some side effect channels, its “tape” can be as large as needed. For example, it can use the internet to store petabytes of state. But if we want to be more pedantic, it can control and observe physical things, making the whole universe its state space.
Which is still finite, so yeah, even more pedantically you are right due to mathematical infinity not existing, but the actual distinction of a Turing machine is “lazy”, if you don’t hit the limit, you may as well reason as if it were a Turing machine.
I'm not sure how that's relevant. Even given infinite space, there still wouldn't exist a Regular Expression (the CS kind) you could write down in finite time that would match only primes.
At least IIRC, it's been a 20+ years since I last took a Discrete Math course :)
Your memory is correct and there is very simple intuition for this. Regular languages are those that can be matched by a machine with bounded memory. So, let's say that bound is "n" bits. Then your machine will not be able to read in unary strings larger than 2^n.
The point is, neither would there exist a non-(Regular Expression) method that could do such, if you're running it on a finite state machine (your computer).
It's more interesting to ask: suppose you did have a magical stick of RAM that had infinite storage, and suppose you augmented your computer to store and query that memory. Then -- yes -- your computer could determine whether any number, however large, is prime. An FSM could not.
Ad absurdum everything would be computable (recognizable) by FSMs, simply by listing all the infinite number of possibilities. listOfAllPrimes.concat(“|”), here you are..
This is just useless pedantism that diminishes the very imporrant categorization of Chomsky.
We don't know how original poster differentiates.
A finite regular expression that checks all numbers for being prime cannot exist already in principle.
The regex itself is generated from a relatively simple DFA based on mod 7 arithmetic on digits using a program (JFLAP apparently, not familiar).
Just from looking at the start of the regex you can see some cool things. For instance, TIL that all numbers of the form 1555....55554 and 85555....5554 are multiples of 7. Fun exercise to prove it.
I am currently transitioning to a different blog engine, so the page is offline right now, but it's still available here
https://web.archive.org/web/20220513113609/neilk.net/blog/20...
https://metacpan.org/pod/Regexp::Exhaustive#Finding-all-divi...
And if you're in Toronto in 3 weeks time, come see Abigail (the original inventor of the prime number regexp) solve the N-Queens problem with regexp:
https://tprc2023.sched.com/event/1LhoB/the-n-queens-problem-...
It is validating whether a string of consecutive 1's has a prime length.
In other words, it can't tell whether "3" is prime, unless you express it as "111".
Much less exciting.
That's comparing apples to oranges, because the two input types have vastly different lengths relative to their value. Unary input length itself grows exponentially with binary input length, for the same numeric value, cancelling out the unary advantage. So unary isn't faster than binary.
In fact, I think it's pretty clear that unary representation is slower and takes more space. Just think about the rough number of steps you would need to add or even multiply two very large numbers, e.g. in a Turing machine. It would be obviously vastly more if the numbers are given in unary rather than in binary.
It really is not, because complexity is fundamentally a function of _the length of the input_ (specifically: it measures how the runtime (or space usage) grows with growing input). If you have longer input, your Turing machine can spend more time to compute its answer. Also note that this assumes that _the input_ is encoded in unary, if you get binary input and need to spend time and space to convert it to unary representation, sure, that will lead to an exponential blow-up.
Edit: > Just think about the rough number of steps you would need to add or even multiply two very large numbers, e.g. in a Turing machine. It would be obviously vastly more if the numbers are given in unary rather than in binary.
First, note that those are not actually pseudo-polynomial, as the number of steps needed for addition (or multiplication) depend on the number of digits, not the numeric values involved. Yet, even here unary encoding _does not have worse complexity_, since you still take polynomially many steps in the length of the input (i.e., the length of the unary encodings of the input values). Yes, all the inputs will be exponentially larger, so in practice it's certainly not the better algorithm, but _the complexity_ is not worse, since that's only concerned with the asymptotic behaviour.
Edit: By the way, as the Wikipedia piece notices, the binary addition algorithm is O(log(n)) in time _relative to the value_, while unary addition is presumably O(n) relative to the value (just writing the numeral out alone takes O(n) steps). So the time complexity is in fact better. Probably the same holds for space complexity. Moreover, only in this case makes a comparison even sense, since we are comparing the same values in both cases, while for the numeral case we would be comparing the lengths of two different types of numerals, apples to oranges.
Why? The Turing machine has no concept of numeric values, it only knows about the length of whatever the input is.
> Adding one million to one million is simply much faster for binary addition than for unary addition, even if unary addition has better complexity relative to its own encoding than binary addition has to its encoding.
From a complexity standpoint, adding one million to one million is O(1), irregardless of the encoding.
> We are interested in complexity relative to the numerical value, not in the length of their respective encodings.
But the complexity relative to the numerical value is not even well-defined, since it _depends_ on the choice of the encoding.
> By the way, as the Wikipedia piece notices, the binary addition algorithm is O(log(n)) in time _relative to the value_, while unary addition is presumably O(n) relative to the value (just writing the numeral out alone takes O(n) steps). So the time complexity is in fact better.
It really is not better. Time complexity is _always_ measured relative to the length of the input, and for binary encoding, the length of the input is O(log(n)), so, taking O(log(n)) steps for the addition is linear, same as the linear time needed for adding numbers in unary encoding (which is basically just copying the input to the output).
> Moreover, only in this case makes a comparison even sense, since we are comparing the same values in both cases, while for the numeral case we would be comparing the lengths of two different types of numerals, apples to oranges.
But _the numeral is not the input_, its _encoding_ is. This is really the whole point, and it is _precisely_ because you get different complexities for different encodings.
That's irrelevant. We are interested in addition, or multiplication, or whatever, which is an operation between numbers (values), and it can be faster or slower depending on which numerals (encoding) are used.
> But the complexity relative to the numerical value is not even well-defined, since it _depends_ on the choice of the encoding.
Yes it depends on the encoding, no it is of course well-defined. You can measure two different algorithms which use two different encodings, relative to the same thing, the value.
> complexity is _always_ measured relative to the length of the input
That's simply not true. There is even a Wikipedia article about it. Even if it calls it "pseudo".
> But _the numeral is not the input_, its _encoding_ is
A numeral is an encoding of a number.
And here, validating a string of ones is simply not what anyone would mean if you said "check whether this number is a prime number" without any qualification, so I agree that whilst still cute, the title is a bit click-baity and not at all what I expected it to be about either.
EDIT: it seems though that the original post, shared via archive.org elsewhere in the thread does do this for what one would normally assume -- and does so by using `(1 x shift) !~` -- so if anyone else was confused, read the original post.
I don't think you've really explained why those two things are different. They're different methods but they're checking the exact same thing.
It's a bit like if you claimed that a program that voice to text is the same as one that does text to text.
Previous discussions (with >25 comments):
2015: https://news.ycombinator.com/item?id=9039537 (67 comments)
2022: https://news.ycombinator.com/item?id=30564287 (121 comments)
A regular expression to check for prime numbers (2007) - https://news.ycombinator.com/item?id=30564287 - March 2022 (121 comments)
A regular expression to check for prime numbers - https://news.ycombinator.com/item?id=9039537 - Feb 2015 (67 comments)
A regular expression to check for prime numbers - https://news.ycombinator.com/item?id=1486158 - July 2010 (13 comments)
Regex to check for prime numbers - https://news.ycombinator.com/item?id=707236 - July 2009 (9 comments)
A regular expression to check for prime numbers - https://news.ycombinator.com/item?id=58780 - Sept 2007 (8 comments)
Extending finite automata to efficiently match Perl-compatible regular expressions https://www.researchgate.net/publication/221325349_Extending...
> Regular expression matching is a crucial task in several networking applications. Current implementations are based on one of two types of finite state machines. Non-deterministic finite automata (NFAs) have minimal storage demand but have high memory bandwidth requirements. Deterministic finite automata (DFAs) exhibit low and deterministic memory bandwidth requirements at the cost of increased memory space. It has already been shown how the presence of wildcards and repetitions of large character classes can render DFAs and NFAs impractical. Additionally, recent security-oriented rule-sets include patterns with advanced features, namely back-references, which add to the expressive power of traditional regular expressions and cannot therefore be supported through classical finite automata.
> In this work, we propose and evaluate an extended finite automaton designed to address these shortcomings. First, the automaton provides an alternative approach to handle character repetitions that limits memory space and bandwidth requirements. Second, it supports back-references without the need for back-tracking in the input string. In our discussion of this proposal, we address practical implementation issues and evaluate the automaton on real-world rule-sets. To our knowledge, this is the first high-speed automaton that can accommodate all the Perl-compatible regular expressions present in the Snort network intrusion and detection system.
----
The awkward part is that a PCRE is demonstrably more powerful than a regular language, but not as powerful as a CFG (you can write CFGs that can't be matched by a PCRE - ([{}]) matching, a^nb^n and so on). So the question that gets interesting is "can every PCRE be matched by a CFG?"
Is it a fork? or is it a non-integer language?
https://en.wikipedia.org/wiki/Chomsky_hierarchy
> Note that the set of grammars corresponding to recursive languages is not a member of this hierarchy; these would be properly between Type-0 and Type-1.
So there's a language between the Turing machine and the linear bounded Turing machine that matches a context sensitive language.
Is the PCRE something between Type-2 and Type-3? or is it a Type-3+I?
https://en.wikipedia.org/wiki/Formal_grammar#Other_forms_of_...
> Many extensions and variations on Chomsky's original hierarchy of formal grammars have been developed, both by linguists and by computer scientists, usually either in order to increase their expressive power or in order to make them easier to analyze or parse. ...
Hmm, there's a reduction of 3-cnf-sat to perl regexps, making them NP-complete. As CFGs are in NP...
https://www.pcre.org/original/doc/html/pcrepattern.html#SEC2...
\( ( [^()]++ | (?R) )* \)
https://regex101.com/r/eBtSTM/1 for a slightly different formulation of that regex.If anyone is seeking a math formula for primes, here is one: https://en.wikipedia.org/wiki/Formula_for_primes
There is also a good YouTube video that explains this: https://www.youtube.com/watch?v=j5s0h42GfvM
the video was very interesting
Not that interesting.
> Is 7 prime?
>
> To know this, the function first generates “1111111” (from “1” \* 7) and tries to
> see if that string does not match /^1?$|^(11+?)\1+$/. If there is no match, then
> the number is prime.
>
> Notice that the regular expression has two parts (separated with the vertical bar |).
>
> The first part is /^1?$/ is trivial and matches with beginning of line (^), an
> optional 1 (1?) and end of line ($) which implies that it matches either the
> empty string or “1”. This simply indicates that calling that function with n==0
> or n==1 will correctly return false (as the “1” \* n will match with the first
> part of the regular expression)
I agree that the article formatting is a bit misleading.I suggest relaxing, everyone. It's a fun story, not a groundbreaking discovery.
"Apologies for any confusion before, and I understand the nature of your request now. It's a fun challenge to think about! While it's not practically useful or efficient, and it is actually quite limited, a creative theoretical solution exists that involves regular expressions. This specific solution only works for very small numbers due to the inherent limitations of regex. It's based on the principle that a number is prime if it cannot be expressed as a product of two smaller natural numbers. We represent the number as a repeated string of 1s, then check for repeating sub-patterns.
For instance, this Perl regex checks if a number, represented as unary (a string of 1s), is prime:
perl Copy code /^1?$|^(11+?)\1+$/ In this regex:
^1?$ checks for the case where you have zero or one '1', which represent the numbers 0 and 1, neither of which are prime. ^(11+?)\1+$ attempts to match the input with a repeating group of '1's. If a match is found, the number is not prime, because a repeating subgroup of '1's represents a divisor. This pattern should be used in a negative sense: if a string of 1s (representing your number in unary form) matches this pattern, it's not prime. If it doesn't match, then it is prime.
Again, this is quite impractical for real use cases as the unary representation is extremely inefficient, and the computation grows quickly with larger numbers, but it's a neat way of showing how flexible and powerful regular expressions can be."