2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[JavaScript] Quine入門

2
Posted at

はじめに

  • JavaScript で Quine (クワイン)に入門してみました
  • Quine は「自身のソースコードと完全に同じ文字列を出力するプログラム」です
  • 基本形から、ちょっとだけ応用をするところまでやってみます

基本形

基本形(1)

Quine の基本形(ほとんどの言語で実現可能な方法)の1つは、プログラムが「ロジック部」と「データ部」からなっていて、ロジック部と同じ内容の「データ」を持っている、というものです。

一例としては、以下のような感じになると思います。

基本形の Quine 例
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() がほぼその役割を満たせます(文字列を括るのはダブルクォーテーションにする必要があります)。

そのため、先ほどのプログラムは以下のように、もっと簡単に記述することができます。

JSON.stringifyを使うように修正した Quine 例
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() を使って、以下のようにも記載が可能です。

Base64で保持する Quine 例
const p="Y29uc29sZS5sb2coImNvbnN0IHA9XCIiK3ArIlwiO1xuIithdG9iKHApKTs=";
console.log("const p=\""+p+"\";\n"+atob(p));

この場合には、出力時にそのまま「データ部」として出力し、デコードして「ロジック部」を出力する形になります。

言うまでもないと思いますが、2行目の「ロジック部」の Base64 エンコード結果を1行目の文字列に入れています。

node での Base64 エンコード例
> btoa('console.log("const p=\""+p+"\";\n"+atob(p));')
'Y29uc29sZS5sb2coImNvbnN0IHA9IiIrcCsiIjsKIithdG9iKHApKTs='
bash での Base64 エンコード例
$ printf "%s" 'console.log("const p=\""+p+"\";\n"+atob(p));' | base64
Y29uc29sZS5sb2coImNvbnN0IHA9XCIiK3ArIlwiO1xuIithdG9iKHApKTs=

もちろん Base64 以外のエンコードも可能ですし、圧縮、暗号化などを使うこともできます(意味があるかは別として)。

基本形(4)

プログラミング言語によっては、文字列の表現方法が複数あって、それを使えばもっと容易に記述ができる場合もあります。

JavaScript ではダブルクォーテーション、シングルクォーテーション、バッククォーテーションで文字列が作れ、特にバッククォーテーションでは、改行なども入れることができます。

ちょっと長くなりますが、以下のようにも記載可能です。

バッククォートでの Quine 例
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回出力して「データ部」と「ロジック部」を出力していましたが、他の方法も考えられます。

一例として、以下のような文字列置換を使うことも可能です。

文字列置換を使った Quine 例
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() のような機能を使えば、「ロジック部」と「データ部」で同じ情報を持つという基本形とは異なり、「データ部」とそれを実行する(「データ部」とは異なる)極小のロジックの形で実現可能です。

evalを使った Quine 例
const p="console.log(`const p=${JSON.stringify(p)};eval(p);`)";eval(p);

一度、名前を付けた変数に入れるのがポイントで、その変数名を参照すれば、再帰的な構造が実現できます。

ソースコード参照型

いくつかのプログラミング言語では、関数、クラス、モジュール、パッケージのようなロジックの塊のソースコードを実行時に取得することが可能です。

あるいは、コンパイル時にソースコードをデータとして埋め込む、実行時にスクリプトファイル(自分自身)を読み込む、といったことができる言語もあります。

これらを使って Quine を実現する方法もあります。つまり「ロジック部」と「データ部」で同じ情報を持つという基本形とは異なり、「データ部」をまるまる省略することが可能です。

JavaScript では、関数オブジェクトを文字列化するとソースコードが取得可能です。

functionのソースコードを取得する Quine の例
function p(){console.log(`${p}p();`);}p();

function で定義された関数だけではなく、アロー関数でも同じように取得できると思います。

アロー関数のソースコードを取得する Quine の例
p=()=>console.log(`p=${p};p();`);p();

他にも、実行時にスクリプトのパスを取得して、読み込み、出力するというアイデアもあります(ちょっとズルっぽいけど)。

common.js形式の自スクリプトファイルを読み込む Quine の例
const fs = require("fs");

const content = fs.readFileSync(__filename, "utf-8");
console.log(content);
モジュール(ESM)形式の自スクリプトファイルを読み込む Quine の例
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 っぽい絵を描くことも非常に容易です。

コメントで絵を描いた Quine 例
p=()=>{console.log(`p=${p};p();`);/*****
****************************************
**  ****  **      **  ******  *******   
**  ****  **  ******  ******  ******  **
**        **      **  ******  ******  **
**  ****  **  ******  ******  ******  **
**  ****  **      **      **      ***   
****************************************
****************************************
**  ******  ***      ***       ***  ****
**  **  **  **  ****  **  ****  **  ****
**  **  **  **  ****  **       ***  ****
****  **  ****  ****  **  ***  ***  ****
****  **  *****      ***  ****  **      
****************************************
** THIS PROGRAM IS A QUINE ******/};p();

応用編

応用編(1)

eval 等を使うと、もっと変わった Quine を作ることも可能です。

例えば、ソースコードを何らかの変形をしたものを、逆変換してから eval() するようなことが可能です。

30文字で折り返した Quine 例
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つの地球の図形での必要な文字数が異なるので、コメント部分の * を削除したり、水増ししたりしている処理もあります。それなので、最後のアメリカ大陸型のソースコードだとコメントの * が多めになったりします)

スペースを含めて空白文字は全部削除してしまうので、スペースなしのコーディングになっています(なので varletconst もないです)。

地球の形のデータは適当なモノクロの絵文字フォントを適当なサイズで画像化してピクセルの有無を取得して、ランレングス圧縮したものです。画像のサイズが小さいとソースコードを埋め込むのに文字が足りなくなったり、大きくすると埋め込む文字が少なくなりすぎたりして、意外とサイズを決めるのに手間取りました。また、崩れてほしくないところで崩れないようにソースコードをちょっと書き換えたりする必要もありました(バックスラッシュが変な位置に入らないように、など。バックスラッシュを避けたり、無駄に長くしたり、短い書き方に書き換えたり)。

地球データの作成例
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
package.json
{
  "dependencies": {
    "canvas": "^3.2.0"
  }
}

今回は変形のランレングス圧縮を使いましたが、別の方法を組んでもいいですし、標準ライブラリなどを使ってもよいと思います。空白文字削除も別の方法で実現することも可能です。好きなように実装ができるのではないかと思います。

参考情報

最近の Quine 業界(そんな業界あるの?)では、なんとなく Ruby が多く使われているイメージがありますね。

おそらく書籍『あなたの知らない超絶技巧プログラミングの世界』あたりが、きっかけの人が多いのではないかと思います。

もし書籍を持っていない人は、情報処理学会で公開されているRubyによる超絶技巧プログラミングが参考になるかもしれません(無料、ユーザー登録などもなしでダウンロード可能です)。

あるいは著者である遠藤氏のGitHubのリポジトリや、公開しているスライドなども参考になると思います。

純粋な Quine だけではなく、複数言語にまたがってリレーする「ウロボロス Quine」や、任意の1文字を削除しても元のソースが再現できる「放射線耐性 Quine」、15パズル Quine など、(なんでこんな物を作ろうと思ったのか、というような)すごいプログラムの数々があります。

英語版の Wikipedia の Quine の項も参考になります(日本語版の Quine の項よりも情報が多い)

いろいろと参考になるんじゃないかと思います。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?