This answer is part of a collaboration between myself and 0'. We both worked on this together, the only reason I'm posting it is because I won Rock, Paper, Scissors.
\Q-->{Q=1};"(",\N,")",\B,{findnsols(N,I,(between(2,inf,I),\+ (between(3,I,U),0=:=I mod(U-1))),L)->append(_,[Y],L),Q is Y*B}.
Try it online!
Explanation
This answer is a perfect exemplar of what makes golfing in prolog fun.
This answer uses Prologs powerful system for definite clause grammars. Here is our grammar ungolfed a bit.
head(1)-->[].
head(Q)-->"(",head(N),")",head(B),{prime(N,Y),Q is Y*B}.
isprime(I):- \+ (between(3,I,U),0 =:= I mod(U-1)).
prime(N,Y):-
findnsols(N,I,(
between(2,inf,I),
isprime(I)
),L),
append(_,[Y],L),!.
The first construction rule is:
head(1)-->[].
This tells Prolog that the empty string corresponds to 1.
Our second rule of construction is a tiny bit more complex.
head(Q)-->"(",head(N),")",head(B),{prime(N,Y),Q is Y*B}.
This tells us that any non empty string contains parentheses around a clause with these same rules, to the right of a clause with these same rules.
It also tells us that the value of this clause (Q
) follows the rule:
{prime(N,Y),Q is Y*B}
Breaking this down, Q
is the product of 2 numbers Y
and B
. B
is just the value of the clause to the left and Y
is the N
th prime where N
is the value of the clause inside the parentheses.
This rule covers both of the formation rules of the factor tree
- Concatenation multiplies
- Enclosure takes the nth prime
Now for the predicate definitions. In the ungolfed version there are two predicates at play (in my actual code I've forward chained the predicates away).
The two relevant predicates here are isprime/1
, which matches a prime number, and prime/2
, which, given N
and Y
, matches iff Y
is the N
th prime. First we have
isprime(I):- \+ (between(3,I,U),0 =:= I mod(U-1)).
This works of a pretty standard definition of primality, we insist that there is no number between 2 and I
, including 2 but not I
that divides I
.
The next predicate is also pretty simple
prime(N,Y):-
findnsols(N,I,(
between(2,inf,I),
isprime(I)
),L),
append(_,[Y],L),!.
We use findnsols
to find the first N
numbers that are prime, we then return the last one. The trick here is that while findnsols
is not guaranteed to find the smallest N
primes, because of the way SWI handles between
it will always find smaller primes sooner. This however means we have to cut to prevent it from finding more primes.
The golfs
We can forward reason in our code twice. Since isprime
is only used once its definition can be moved inside of prime
. The next one is to move prime
directly inside of the DCG, however since we use a cut in prime
to prevent findnsols
from producing too many primes we have a bit of an issue. The cut, cuts the entire DCG instead of just the bit we want. After a bit of documentation digging we found that once/1
could be used to cut just this portion but not the entire DCG. However more documentation digging revealed that the ->
operator could also be used to perform a similar task. The ->
operator is roughly equivalent to ,!,
so we moved our cut to the other side of append/3
and replaced it with ->
.
In SWI-Prolog predicates (and rules) can be give operators as names which allows us to drop the parentheses normally required. Thereby we can save 6 bytes by calling the rule \
.