Understanding quine-central
: how do quine loops work?
Yesterday another Recurser introduced me to
quine-central
, which is:
[A] Haskell program that generates a Haskell program that prints out a Perl program that prints out a Python program that prints out a Ruby program that prints out a C program that prints out a Java program that prints out a Rust program that prints out an OCaml program that prints out a Swift program that prints out a Racket program that prints out a Javascript program that prints out the first Haskell program generated.
“How the hell does that work?” I thought to myself. Maybe you’re thinking that too, so here’s my attempt at an explanation.
quine-central
can produce quine loops of any length, so
let’s first think about a loop of three programming languages:
Haskell, Ruby, and C. In this case, quine-central
prints
a Haskell program that prints a Ruby program that prints a C program
that prints the first Haskell program generated.
The generated Haskell program has two lines. The first line defines a
function called q
that takes three arguments.
q
builds and returns a string that contains its first
argument, then a newline, and finally some Ruby code that calls a
function called q
. This call to q
in Ruby
takes the same arguments but rotated one position to the left. That
is, it takes the second argument to the Haskell q
, then
the third, then the first.
In the actual output, {a0}
is replaced with the value of
a0
. Same goes for the other variables. Also, this isn’t
quite the actual code. I simplified it a little for demonstration
purposes.
The generated Haskell program's second line is a main method that
calls the function q
defined on the first line and prints
the result. Each argument passed to q
is the source code
for a definition of q
in a language in the loop. The
first argument is a definition of q
in Ruby, the second
in C, the last in Haskell.
main = putStrLn $ q "{Ruby definition of q}"
"{C definition of q}"
"{Haskell definition of q}"
In all of these definitions, q
does the same thing as it
does in the Haskell program. It prints its first argument, then prints
a call to a function called q
, but in the next language
in the loop. It also rotates its arguments one position to the left
before printing them.
When the program is run, q
starts by printing its first
argument, which is a declaration of q
in Ruby. Then, it
prints a call to q
in Ruby. The arguments to this call
are the second, third, and first arguments passed to q
in
the Haskell program, which are definitions of q
in C,
Haskell, and Ruby respectively.
The output of the Haskell program is a Ruby program with a very
similar structure. Both have two lines. Both declare a function
q
on the first line. q
does the same thing
in both programs. And both call q
on the second line,
passing in definitions of q
for the rest of the loop.
When this Ruby program is run, it in turn prints a C program with the same structure. And that C program in turn prints a Haskell program with the same structure. In fact, it prints the original Haskell program, closing the loop.
In the next post, I’ll discuss how
quine-central
generates these programs in the first
place. We’ll also do away with some of the simplifications I made to
the programs in this post and look at how
quine-central
handles the problems posed by string
escaping.