はじめに
- JavaScript で Quine (クワイン)に入門してみました
- Quine は「自身のソースコードと完全に同じ文字列を出力するプログラム」です
- 基本形から、ちょっとだけ応用をするところまでやってみます
基本形
基本形(1)
Quine の基本形(ほとんどの言語で実現可能な方法)の1つは、プログラムが「ロジック部」と「データ部」からなっていて、ロジック部と同じ内容の「データ」を持っている、というものです。
一例としては、以下のような感じになると思います。
const p="function e(d){return d.replace(/\\\\/g,\"\\\\\\\\\").replace(/\"/g,'\\\\\"').replace(/\\n/g,\"\\\\n\");}\nconsole.log(\"const p=\\\"\"+e(p)+\"\\\";\\n\"+p);";
function e(d){return d.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\n/g,"\\n");}
console.log("const p=\""+e(p)+"\";\n"+p);
ちょっと長いですが、2~3行目のロジックと、1行目の文字列の中身が一致しているのがわかると思います。
そしてロジック部(3行目)では、それを使って(2回出力して)、自分自身と同じになるように出力しています。
基本形(2)
多くのプログラミング言語では文字列として定義する場合に、特定の文字をエスケープするなど一種のエンコードをする必要があります。そのエンコードされた文字列は評価時に自動でデコードされるので、「ロジック部」として出力するには単にそのまま出力すればよいだけです。
逆に、「データ部」の一部としてエンコードして出力するために、前掲のプログラムでは2行目でそのエンコード処理を定義していました。
しかし、一部の言語ではこのエンコード処理と同じことを、組み込み関数などで持っている場合があります。
例えば JavaScript の場合には、JSON.stringify() がほぼその役割を満たせます(文字列を括るのはダブルクォーテーションにする必要があります)。
そのため、先ほどのプログラムは以下のように、もっと簡単に記述することができます。
const p="console.log(\"const p=\"+JSON.stringify(p)+\";\\n\"+p);";
console.log("const p="+JSON.stringify(p)+";\n"+p);
基本形(3)
基本形(1)および(2)では、ロジックと全く同じ文字列としてエンコードしていましたが、別にそれにこだわる必要はありません。例えば Base64 でエンコードした文字列を持っておくことも可能です。JavaScript では Base64 のデコードは atob() で処理できます。
atob() を使って、以下のようにも記載が可能です。
const p="Y29uc29sZS5sb2coImNvbnN0IHA9XCIiK3ArIlwiO1xuIithdG9iKHApKTs=";
console.log("const p=\""+p+"\";\n"+atob(p));
この場合には、出力時にそのまま「データ部」として出力し、デコードして「ロジック部」を出力する形になります。
言うまでもないと思いますが、2行目の「ロジック部」の Base64 エンコード結果を1行目の文字列に入れています。
> btoa('console.log("const p=\""+p+"\";\n"+atob(p));')
'Y29uc29sZS5sb2coImNvbnN0IHA9IiIrcCsiIjsKIithdG9iKHApKTs='
$ printf "%s" 'console.log("const p=\""+p+"\";\n"+atob(p));' | base64
Y29uc29sZS5sb2coImNvbnN0IHA9XCIiK3ArIlwiO1xuIithdG9iKHApKTs=
もちろん Base64 以外のエンコードも可能ですし、圧縮、暗号化などを使うこともできます(意味があるかは別として)。
基本形(4)
プログラミング言語によっては、文字列の表現方法が複数あって、それを使えばもっと容易に記述ができる場合もあります。
JavaScript ではダブルクォーテーション、シングルクォーテーション、バッククォーテーションで文字列が作れ、特にバッククォーテーションでは、改行なども入れることができます。
ちょっと長くなりますが、以下のようにも記載可能です。
const p=`
const q=String.fromCharCode(96);
console.log("const p="+q+p+q+";"+p);`;
const q=String.fromCharCode(96);
console.log("const p="+q+p+q+";"+p);
ロジックである4~5行目と、文字列の2~3行目とがほぼ同じ(文字列の閉じクォートとセミコロン以外)になっています。
基本形(5)
今までは同じデータを2回出力して「データ部」と「ロジック部」を出力していましたが、他の方法も考えられます。
一例として、以下のような文字列置換を使うことも可能です。
const p="const p=FOO;\nconsole.log(p.replace(/FOO/,JSON.stringify(p)));";
console.log(p.replace(/FOO/,JSON.stringify(p)));
replace() でグローバルフラグなしの正規表現を対象に置換しているため、文字列中の先頭の FOO のみ置換がされます。
基本形(1)~(4)と違って、構造自体が再帰的な構造(かつ遅延評価的)になっているのが特徴です。
他にもいろいろな形が考えられると思います。
言語独自の機能を使った Quine
古典的な(当初の) Quine は、クリーネの再帰定理から導かれる不動点関数(プログラム)としての Quine であり、この定理によれば任意の計算可能な文字列を出力できるプログラミング言語では計算可能関数をそれ自身を用いて記述することができる、としています。つまり、ほとんどプログラミング言語では、特定の言語に特有の機能などを使わずに Quine を実装することか可能です。前述の基本形(1)はこのパターンであり、その他の基本形もそれを変形したものになっています。
それとはちょっと異なり、言語の特有の機能を使うと容易に Quine が実現できる場合があります。
文字列評価・実行型
いくつかの言語では、「ソースコードを表す文字列」を評価して実行することが可能です。
JavaScript では eval() で評価、実行することが可能ですね。
この eval() のような機能を使えば、「ロジック部」と「データ部」で同じ情報を持つという基本形とは異なり、「データ部」とそれを実行する(「データ部」とは異なる)極小のロジックの形で実現可能です。
const p="console.log(`const p=${JSON.stringify(p)};eval(p);`)";eval(p);
一度、名前を付けた変数に入れるのがポイントで、その変数名を参照すれば、再帰的な構造が実現できます。
ソースコード参照型
いくつかのプログラミング言語では、関数、クラス、モジュール、パッケージのようなロジックの塊のソースコードを実行時に取得することが可能です。
あるいは、コンパイル時にソースコードをデータとして埋め込む、実行時にスクリプトファイル(自分自身)を読み込む、といったことができる言語もあります。
これらを使って Quine を実現する方法もあります。つまり「ロジック部」と「データ部」で同じ情報を持つという基本形とは異なり、「データ部」をまるまる省略することが可能です。
JavaScript では、関数オブジェクトを文字列化するとソースコードが取得可能です。
function p(){console.log(`${p}p();`);}p();
function で定義された関数だけではなく、アロー関数でも同じように取得できると思います。
p=()=>console.log(`p=${p};p();`);p();
他にも、実行時にスクリプトのパスを取得して、読み込み、出力するというアイデアもあります(ちょっとズルっぽいけど)。
const fs = require("fs");
const content = fs.readFileSync(__filename, "utf-8");
console.log(content);
import { fileURLToPath } from "url";
import fs from "fs";
const filename = fileURLToPath(import.meta.url);
const content = fs.readFileSync(filename, "utf-8");
console.log(content);
これらの場合には、コメントや改行位置なども保存されるため、ソースコードが壊れない範囲で任意の整形をすることも可能です。例えば、コメントで Hello World っぽい絵を描くことも非常に容易です。
p=()=>{console.log(`p=${p};p();`);/*****
****************************************
** **** ** ** ****** *******
** **** ** ****** ****** ****** **
** ** ** ****** ****** **
** **** ** ****** ****** ****** **
** **** ** ** ** ***
****************************************
****************************************
** ****** *** *** *** ****
** ** ** ** **** ** **** ** ****
** ** ** ** **** ** *** ****
**** ** **** **** ** *** *** ****
**** ** ***** *** **** **
****************************************
** THIS PROGRAM IS A QUINE ******/};p();
応用編
応用編(1)
eval 等を使うと、もっと変わった Quine を作ることも可能です。
例えば、ソースコードを何らかの変形をしたものを、逆変換してから eval() するようなことが可能です。
p=()=>eval(`t=("p="+p+";p();")
.replace(/\\s/g,"");o=t.match(
/.{1,30}/g).join("\\n");consol
e.log(o);//___This_program_is_
Quine`.replace(/\s/g,""));p();
これは文字列を、30文字で折り返すようにして定義しています。
そして空白文字(つまり改行コード)を削除してから eval() しています。
ちなみに、バックスラッシュが変な位置に来るとうまく動かないので、折り返す桁数やコードの細かい箇所を調整する必要が出てくるかもしれません。また空白が削除されるので、空白文字が使えないのも注意が必要です(改行だけ削除するようにしてもよいかもしれません)。
応用編(2)
応用編(1)では、単に30文字で折り返していましたが、もっと凝った変換をすることも可能です。
純粋な Quine というわけではありませんが、以下のようなものも作れます(複数回実行で元に戻る「循環 Quine」のようなもの)。
例えば、以下のソースコードを見てください。
n=1;(p=()=>eval(`t=("n=${(n+1)%3};(p="+p+")();").r
eplace(/\\s/g,"");d=["ZZZZZZLoZEwXvDcSyEcPyHbNqDbL
bLsDaMbKtQbJsScIgCeWcJdFeVbKbAaVcHbLbGaEcQbNcGbGiD
cPcMmAcSdGrXdEdFdZ GkZZZZO","ZZZZZZK
qZCyVcCyRcCdDa AqOcDdNlMbGgH
fBeLbFqAjAb JcEs BgBcIcFuHc
IcGuGcJbN lIbKc NjIcLbNj
IbNcLiBb DcQcKgGc UcJeFcY
eMeZHiZ ZZZP ","ZZZZZZLoZ EeBaJe
XhFcIc SoAf HcPuLbNbBoPbL bElQc
KbGdXb JcLcTcIcPaAeLcIcQ jHbKb
PnEbL bPmDbNcPjCcPcOhCdSd KfDd
WeHeB dZF mZZZZN"][n];a=t.replace (/\\
*{2,}/ ,m=>"* ".repeat(d.split("").f ilter
(m=>m< " a").map(m=>m.charCodeA t(0)&31) .redu
ce((a,c )=>a+c) +m.le ngth-t.length)).s plit("
");o=d.r eplace( /./g,m= >{l= m.charC
odeAt(0)& 31;return(m>" Z "?"\\x20
".repeat(l) :a.spli ce(0,l).jo
in(""))}).matc h(/.{ 1,50}/ g).join("\\n"
);console.log(o)/*** *******************
**************************_This_program_is_Quine_*
***********_yoshi389111_*/`.replace(/\s/g,"")))();
これを実行すると、以下の結果が出力されます。
n=2;(p=()=>eval(`t=("n=${(n+1)%3};(p="+p+")();").r
eplace(/\\s/g,"");d=["ZZZZZZLoZEwXvDcSyEcPyHbNqDbL
bLsDaMbKtQbJsScIgCeWcJdFeVbKbAaVcHbLbGaEcQbNcGbGiD
cPcMmAcSdGrXdEdFd ZGkZZZZO","ZZZZZ
ZKqZCyVcCyRcC dDaAqOcDdNlM
bGgHfBeLbF qAj AbJcEsBgB
cIcFuHcIc GuG cJbN l IbKcNjIc
LbNjIbN cLiB bDcQcKgGcUcJeF cYeMeZ
HiZZZZP ","ZZZZ ZZLoZEeB aJ eXhFcI
cSoAfH cPuLbN b B oPbLb
ElQcK bGdXb Jc Lc TcIc
PaAeL cIcQjH bKbPnEbL bPmD
bNcPj CcPcOhC dSdKfDd WeHe
BdZFmZ ZZZN"][n];a=t. replace(/ \\*{2
,}/,m= >"*".repeat(d. split("") .filt
er(m=>m <"a").map(m=>m .charCode At(0)&
31).redu ce((a,c)=>a+ c) +m.l ength-t
.length)). split("");o =d.repl ace(/./g,
m=>{l=m.char CodeAt(0)& 31;ret urn(m>"Z"?"
\\x20".repeat( l):a.splice(0 ,l).join(""))
}).match(/.{1,50}/g). join("\\n");console.
log(o)/*******************_This_program_is_Quine_*
***********_yoshi389111_*/`.replace(/\s/g,"")))();
さらにこのプログラムを動かすと、以下が出力されます。
n=0;(p=()=>eval(`t=("n=${(n+1)%3};(p="+p+")();").r
eplace(/\\s/g,"");d=["ZZZZZZLoZEwXvDcSyEcPyHbNqDbL
bLsDaMbKtQbJsScIgCeWcJdFeVbKbAaVcHbLbGaEcQbNcGbGiD
cPcMmAcSdGrXdEdFdZ GkZZZZO","ZZZZZZK
qZCyVcCyRcCdDa Aq OcDdNlMbGg HfBeLbFqAjAbJ
cEsBgBcIcFu HcIcGu GcJbNlIbK cNjIcLbNjI
bNcLiBbDc Q cKgGcUcJ eFcYeMeZ
HiZZZZP" ,"ZZZZZZLoZE eBaJeXh
FcIcSoA fH cPuLbNbBoPbLbElQ cKbGdX
bJcLcT cIcPa AeLcIcQjHbKbPnEbL bPmDb
NcPjCc PcOhCdS dKfDdWeHeBdZFmZZZZN"][n] ;a=t.
repla ce(/\\*{2,}/ ,m=>"*".repeat(d.spl it("
").fi lter(m=>m<"a").m a p(m=>m.charC odeA
t(0)& 31).reduce((a,c)= >a+c)+m. lengt
h-t.le ngth)).split("") ;o=d. repla
ce(/./g ,m=>{l=m.charCod eAt( 0)&31;
return(m >"Z"?"\\x20".rep eat (l):a.s
plice(0,l ).join(""))}).m atc h(/.{1,5
0}/g).join( "\\n");cons ole. log(o)/***
************* ******** ** *************
******************* ******************
**************************_This_program_is_Quine_*
***********_yoshi389111_*/`.replace(/\s/g,"")))();
(最後の方が、ちょっとコメントの * ばかりでちょっとアレですが)これを実行すると、最初の「地球(アジア・オーストラリア)」の状態に戻ります。
地球が回転している、というにはちょっと粗いですが、こんなことも思ったよりも簡単に実現できます(ちょっと手間がかかって面倒かもしれませんが、技術的難易度はそれほど高くない)。
どうやって作ったかというと、まず上記のソースコードは、実際には以下のような内容になっています。
n=0;
(p=()=>eval(`
t=("n=${(n+1)%3};(p="+p+")();").replace(/\\s/g,"");
d=["ZZZZZZLoZEwXvDcSyEcPyHbNqDbLbLsDaMbKtQbJsScIgCeWcJdFeVbKbAaVcHbLbGaEcQbNcGbGiDcPcMmAcSdGrXdEdFdZGkZZZZO",
"ZZZZZZKqZCyVcCyRcCdDaAqOcDdNlMbGgHfBeLbFqAjAbJcEsBgBcIcFuHcIcGuGcJbNlIbKcNjIcLbNjIbNcLiBbDcQcKgGcUcJeFcYeMeZHiZZZZP",
"ZZZZZZLoZEeBaJeXhFcIcSoAfHcPuLbNbBoPbLbElQcKbGdXbJcLcTcIcPaAeLcIcQjHbKbPnEbLbPmDbNcPjCcPcOhCdSdKfDdWeHeBdZFmZZZZN"][n];
a=t.replace(/\\*{2,}/,m=>"*".repeat(d.split("").filter(m=>m<"a").map(m=>m.charCodeAt(0)&31).reduce((a,c)=>a+c,0)+m.length-t.length)).split("");
o=d.replace(/./g,m=>{
l=m.charCodeAt(0)&31;
return(m>"Z"
? "\\x20".repeat(l)
: a.splice(0,l).join(""))
})
.match(/.{1,50}/g)
.join("\\n");
console.log(o)
/**************************_This_program_is_Quine_************_yoshi389111_*/`.replace(/\s/g,"")))();
文字列の中で見にくいですが d という配列に、どのような形に整形するかの図形の情報を持っています。
d の要素は、一種のランレングス圧縮されたデータです。ランレングス圧縮は、同じデータがいくつか連続するようなデータや画像の圧縮が可能な、比較的簡易的な圧縮方法で、通常は繰り返す長さと、繰り返す情報をペアとして、それの配列を持つ感じです。
しかし、ここでは変形ランレングス圧縮として、英字の大文字だとドットあり、小文字だとドットなしを表し、A-Z(あるいはa-z)で長さ(1~26)を表わしています。26個以上のデータは2文字以上で表わす感じですね。
もちろん d の3要素が、地球の形のデータになっています。
それ以降のロジックは、ランレングス圧縮の展開をして、ソースコードの文字列の1文字1文字をその形に当てはめていって、最後に1行50文字で折り返す処理です。
(実際には3つの地球の図形での必要な文字数が異なるので、コメント部分の * を削除したり、水増ししたりしている処理もあります。それなので、最後のアメリカ大陸型のソースコードだとコメントの * が多めになったりします)
スペースを含めて空白文字は全部削除してしまうので、スペースなしのコーディングになっています(なので var や let 、 const もないです)。
地球の形のデータは適当なモノクロの絵文字フォントを適当なサイズで画像化してピクセルの有無を取得して、ランレングス圧縮したものです。画像のサイズが小さいとソースコードを埋め込むのに文字が足りなくなったり、大きくすると埋め込む文字が少なくなりすぎたりして、意外とサイズを決めるのに手間取りました。また、崩れてほしくないところで崩れないようにソースコードをちょっと書き換えたりする必要もありました(バックスラッシュが変な位置に入らないように、など。バックスラッシュを避けたり、無駄に長くしたり、短い書き方に書き換えたり)。
地球データの作成例
const { createCanvas, registerFont } = require("canvas");
const fs = require("fs");
registerFont("seguisym.ttf", { family: "emoji" });
function createCompressedEmojiBanner(emoji, size) {
const canvas = createCanvas(size, size);
const ctx = canvas.getContext("2d");
// 背景白
ctx.fillStyle = "white";
ctx.fillRect(0, 0, size, size);
// 絵文字を描画
ctx.font = `${size * 0.9}px emoji`;
ctx.textAlign = "center";
ctx.textBaseline = "alphabetic";
// メトリクス取得
const metrics = ctx.measureText(emoji);
const ascent = metrics.actualBoundingBoxAscent || size * 0.8;
const descent = metrics.actualBoundingBoxDescent || size * 0.2;
const textHeight = ascent + descent;
const centerY = size / 2 + (ascent - textHeight / 2);
ctx.fillStyle = "black";
ctx.fillText(emoji, size / 2, centerY);
// ピクセルデータを取得
const imgData = ctx.getImageData(0, 0, size, size).data;
// 白黒判定してターミナルに出力
let output = "";
for (let y = 0; y < size; y += 2.2) {
for (let x = 0; x < size; x += 1) {
const idx = (Math.floor(y) * size + x) * 4;
const r = imgData[idx];
const g = imgData[idx + 1];
const b = imgData[idx + 2];
const brightness = (r + g + b) / 3;
output += brightness < 128 ? " " : "#";
}
output += "\n";
}
// 変形ランレングス圧縮をする
// # が連続している場合には大文字の'A'〜'Z'で表現
// 空白が連続している場合には小文字の'a'〜'z'で表現
// 27文字以上連続する場合には ZA のように26文字で分割して表現する
const compressed = output.replaceAll("\n", "").replace(/(#{1,26}| {1,26})/g, (m) => {
const baseChar = m[0] === "#" ? "A" : "a";
return String.fromCharCode(baseChar.charCodeAt(0) + m.length - 1);
});
return compressed;
}
const size = 50;
console.log(createCompressedEmojiBanner("🌏", size)); // asia
console.log(createCompressedEmojiBanner("🌍", size)); // europa
console.log(createCompressedEmojiBanner("🌎", size)); // america
{
"dependencies": {
"canvas": "^3.2.0"
}
}
今回は変形のランレングス圧縮を使いましたが、別の方法を組んでもいいですし、標準ライブラリなどを使ってもよいと思います。空白文字削除も別の方法で実現することも可能です。好きなように実装ができるのではないかと思います。
参考情報
最近の Quine 業界(そんな業界あるの?)では、なんとなく Ruby が多く使われているイメージがありますね。
おそらく書籍『あなたの知らない超絶技巧プログラミングの世界』あたりが、きっかけの人が多いのではないかと思います。
もし書籍を持っていない人は、情報処理学会で公開されているRubyによる超絶技巧プログラミングが参考になるかもしれません(無料、ユーザー登録などもなしでダウンロード可能です)。
あるいは著者である遠藤氏のGitHubのリポジトリや、公開しているスライドなども参考になると思います。
純粋な Quine だけではなく、複数言語にまたがってリレーする「ウロボロス Quine」や、任意の1文字を削除しても元のソースが再現できる「放射線耐性 Quine」、15パズル Quine など、(なんでこんな物を作ろうと思ったのか、というような)すごいプログラムの数々があります。
英語版の Wikipedia の Quine の項も参考になります(日本語版の Quine の項よりも情報が多い)
いろいろと参考になるんじゃないかと思います。