JavaScriptでロゴ入りオリジナルQuineを作ってみた

2022/03/11更新

「個人的実験」のカテゴリでは、私が個人的な興味に基づいて、単に勉強や考察のためにやってみたことをまとめています。実用性を意識した内容ではありませんので、ご了承下さい。

目次

導入

はじめに

TwitterなどのSNSで当サイトをシェアしていただくと、当サイトが設定している以下のOGP画像が表示される。この画像に書かれているコードがJavaScriptのコードであることはすぐお気づきになるだろう。

og_image.png

テキストで表示すると、以下の通りである。

f=function(c){r=RegExp(['\x27.*?\x27','".*?"','[$_A-Za-z]+','\\d+','\\)=>',/*
*/'[^\\(\\)\\[\\]{}$_A-Za-z0-9\x27"\/]+','.'].join('|'),'g');s=('f='+f+';f("'
).replace(RegExp(['[\\n\x20]','\/\\*'+ '[^]*?'+'\\*\/'].join('|'),'g'),'')+c+
'")';d=[0,0,38,1,0,37,3,0,35,7,0,34,9   ,0,32,13,0,30,7,3,7,0,29,7,5,7,0,27/*
*/,23,0,26,25,0,24,7,15,7,0,0,0,-1]       ;p=(()=>{g=String.fromCharCode;p=''
;for(i=0;1000>i;i++){p+=g(97+(i*7)         %26)+'='+i*3%10+';'}return(p)})();
t=s.match(r);m='';b=0;a=(n,k)=>{             q=m;while(t[0]&&(m?n-0:n-2)>=(q+
t[0]).length){q+=t.shift()}if(       m){       q=q.padStart(n,p.substring(b,b
+=n));m=''}else{if(t[0]&&(q+t       [0]).       length===n){q+=t.shift()}else
{if(t[0]&&t[1]&&(q+t[0]+t[1                       ]).length===n){q+=t.shift()
+t.shift()}else{if(k){q=/*                         z=9;*/'\x2f*'+('*\x2f'+q).
padStart(n-2,p.substring       (b,b+=n))}else{       q=(q+'\x2f*').padEnd(n,p
.substring(b,b+=n));m='*\x2f'}}}}return(q)};w=77;x=0;o='';for(i=0;d.length>i;
i++){e=d[i];if(0>=e){o+=a(w-x,0>e)+'\n';x=0}else{o+=a(e).padEnd(z=e+d[++i]);x
/*7;*/+=z}}console.log(o)};f("(C)2021 Azicore https://azisava.sakura.ne.jp/")

このコードは、ブラウザのコンソールや、Node.jsのnodeコマンドなどで実行することができ、console.log()によって以下の文字列を出力する。

f=function(c){r=RegExp(['\x27.*?\x27','".*?"','[$_A-Za-z]+','\\d+','\\)=>',/*
*/'[^\\(\\)\\[\\]{}$_A-Za-z0-9\x27"\/]+','.'].join('|'),'g');s=('f='+f+';f("'
).replace(RegExp(['[\\n\x20]','\/\\*'+ '[^]*?'+'\\*\/'].join('|'),'g'),'')+c+
'")';d=[0,0,38,1,0,37,3,0,35,7,0,34,9   ,0,32,13,0,30,7,3,7,0,29,7,5,7,0,27/*
*/,23,0,26,25,0,24,7,15,7,0,0,0,-1]       ;p=(()=>{g=String.fromCharCode;p=''
;for(i=0;1000>i;i++){p+=g(97+(i*7)         %26)+'='+i*3%10+';'}return(p)})();
t=s.match(r);m='';b=0;a=(n,k)=>{             q=m;while(t[0]&&(m?n-0:n-2)>=(q+
t[0]).length){q+=t.shift()}if(       m){       q=q.padStart(n,p.substring(b,b
+=n));m=''}else{if(t[0]&&(q+t       [0]).       length===n){q+=t.shift()}else
{if(t[0]&&t[1]&&(q+t[0]+t[1                       ]).length===n){q+=t.shift()
+t.shift()}else{if(k){q=/*                         z=9;*/'\x2f*'+('*\x2f'+q).
padStart(n-2,p.substring       (b,b+=n))}else{       q=(q+'\x2f*').padEnd(n,p
.substring(b,b+=n));m='*\x2f'}}}}return(q)};w=77;x=0;o='';for(i=0;d.length>i;
i++){e=d[i];if(0>=e){o+=a(w-x,0>e)+'\n';x=0}else{o+=a(e).padEnd(z=e+d[++i]);x
/*7;*/+=z}}console.log(o)};f("(C)2021 Azicore https://azisava.sakura.ne.jp/")

そう、元のコードと全く同じである。つまりこれはQuineなのだ。ここでは、このロゴ入りオリジナルQuineの完成までの道のりを紹介する。

Quineとは

Quine(クワイン)とは、そのソースコードと全く同じ文字列を出力するプログラムのことである。先に紹介した当サイトのOGP画像のコードは、実行するとそのコードと全く同じ文字列を出力するため、Quineであると言える。

Quineの議論をする際は通常、そのプログラムは入力を受け付けないものとする。入力を受け付けて良いとなると、単にソースコードを入力としてそれをそのまま出力するプログラムを作れば実現できてしまい、何の面白味も無くなってしまうからである。

Quineは、生物のように自己複製をしているように見えるという点で神秘的に感じられるが、実用性はあまり無く、プログラミングの世界における娯楽の一種であると言える。とはいえ、少し考えていただくと分かると思うが、実際にQuineを実装するにはある程度のテクニックが必要となる。今回私が作ったQuineは、見た目がロゴのアスキーアートになっているというのが特徴であるが、世の中にはもっとはるかに驚異的なQuineの作品が数多く存在する。興味を持たれた方は検索されることをお勧めする。

方針の決定

Quineの基本形

Quineを作る試みは、はるか昔から多くの人によってなされてきているため、すでにある程度の方針や答えは存在している。JavaScriptにおけるQuineの基本形としてよく紹介されているのは、関数の文字列化を使用した以下の形である。

f = function() {
    console.log('f = ' + f + ';\nf();');
};
f();

関数fを定義して、それを実行しているだけである。fの中にはconsole.log()があり、これによって出力が行なわれているが、その中にf自身が文字列として連結されている。実は、関数を文字列化(toString()の実行)すると、その関数を定義したコードそのもの(上記の場合function() {から}まで)を文字列として得ることができる。なので、その前後に残りのf = ;\nf();を付け足せば、上記のコードと全く同じになるという仕組みである。

テンプレート文字列の利用

ただ、これだと、改行や空白で見た目を整える必要があるアスキーアートにしていくのは難しい。例えばconsoleは7文字あるが、この単語の途中で改行することはできない。そこで、改行が自由に挿入できるテンプレート文字列でコードを記述し、それをevalする方法を考える。

f = function() {
    eval(`console.log('f = ' + f + ';\\nf();')`);
};
f();

fの中身をテンプレート文字列(`~`)とevalで囲んだだけである。\nのエスケープが二重に必要になっていることに注意する。

このテンプレート文字列内で改行や空白を自由に挿入し、evalに渡す前にreplace()で全て取り除くことにする。空白も全て取り除かれるので、空白を使わずにコードを書く必要がある。例えばreturn xreturn(x)else if~else{if~}などと書く必要が出てくる。また、テンプレート文字列の外側は自由に改行はできないので、なるべく短くなるよう、改行や空白は取り除き、function()()=>に変更してみる。

f=()=>{eval(`console.log('f='+f+';f();')`.replace(/[\n ]/g,''));};f();

こうすると、`~`のテンプレート文字列の中だけは、改行や空白、パディングのためのコメントなどが自由に挿入できるようになる。例えば、以下のようにしても正常に実行できる。

f=()=>{eval(`
console.l
og('f='+f
+';f();')
//hogeeee
//fugaaaa
`.replace(/[\n ]/g,''));};f();

しかし、コードが完成したらOGP画像にしようと考えていた私はこの時点であることに気づいた。この方式だと、コードの大部分がテンプレート文字列となるため、シンタックスハイライトが非常につまらないものとなってしまうのだ。やはりコードはシンタックスハイライトでカラフルに色がついている方がよいと思い、この方式はやめることにした。

トークン化によるコード整形

テンプレート文字列などを使わず、生のコードに改行や空白を入れて整形するとなると、改行や空白を挿入できる位置を見極める必要がある。そのためにはまずコードをトークン(JavaScriptのコードとして意味を持つ最小単位)に分解し、トークンの境界でのみ改行や空白を挿入するようにしなければならない。

これをふまえて、以下のような方針を取ることにする。

  1. まず、Quineの基本形と同じように、関数自身を文字列化し、前後に足りない部分(f=など)を付け足す。

  2. 1.から、改行、空白、コメントを全て取り去る。

  3. 2.をトークンに分解する。

  4. あらかじめ定義したアスキーアートに当てはまるよう、3.で分解したトークンの境界に改行、空白、コメントを挿入していく。

  5. 完成した文字列を出力する。

全体のコードの形としては、以下のようになる。

f = function() {
    s = 'f=' + f + ';f()';                      // 1. Quineの基本形
    s = s.replace(/[\n ]|\/\*[^]*?\*\//g, '');  // 2. 改行や空白の除去
    t = トークンに分解(s);                      // 3. トークン分解
    tに改行や空白を挿入して整形                 // 4. 整形
    console.log(完成した文字列);                // 5. 出力
};
f();

コードの中に著作権表示を入れたかったので、最後のf()の引数部分に文字列リテラルで著作権表示を入れることにした。これは、1.の段階でコードに付け足してしまえば、その後の処理は問題無く進めることができる。

また、後述するが、トークンの分解に際して、簡単のため正規表現リテラルは対応しないことにした。そのため、2.で使用する正規表現はRegExp()と文字列リテラルで表記する。正規表現内の半角スペースも2.自身の処理で取り除かれてしまうのを防ぐため、\x20と表記しておく。

1.、2.の処理を修正して結合したものが以下である。

f = function(c) {
    s = ('f=' + f + ';f("').replace(RegExp([
        '[\\n\x20]',
        '\/\\*[^]*?\\*\/'
    ].join('|'), 'g'), '') + c + '")';
    … 略 …
};
f("(C)2021 Azicore https://azisava.sakura.ne.jp/");

その他の具体的な処理については、次の節以降で順番に解説していく。

実装

アスキーアートの定義

前節で定めた方針に沿って、いよいよ実装していく。

まず、作成するコードのサイズは、目標とするOGP画像のサイズやフォントサイズ、コードの量などをいろいろ考えた結果、77文字×15行とすることにした。模様は、Faviconにもなっている当サイト「AZISAVA」のロゴの「A」のマークである。

#############################################################################
#############################################################################
###################################### ######################################
#####################################   #####################################
###################################       ###################################
##################################         ##################################
################################             ################################
##############################       ###       ##############################
#############################       #####       #############################
###########################                       ###########################
##########################                         ##########################
########################       ###############       ########################
#############################################################################
#############################################################################
#############################################################################

このアスキーアートの情報を持っておくには、単純にコードと空白がそれぞれ何文字連続しているかが分かりさえすればよい。例えば、3行目であれば、「コードが38文字連続、次いで空白が1文字、さらにコードが38文字連続して終了」といった具合である。このうち、最後のブロックは1行の文字数(77文字)から逆算できるため、それ以外のブロックの数字だけが分かればよい。このように「残り全て」を0で表すとすると、1行目は[0]、3行目は[38, 1, 0]、8行目は[30, 7, 3, 7, 0]などと表せる。なお、0が来るとその行は終わりと判別できるため、これらは連続して記述できる。以上をふまえて、アスキーアートの定義は、以下のような1つの配列でできる。

d = [
    0,
    0,
    38, 1, 0,
    37, 3, 0,
    35, 7, 0,
    34, 9, 0,
    32, 13, 0,
    30, 7, 3, 7, 0,
    29, 7, 5, 7, 0,
    27, 23, 0,
    26, 25, 0,
    24, 7, 15, 7, 0,
    0,
    0,
    -1
];

後の節で説明するが、最終行かどうかを区別できる必要があるため、最終行だけは0の代わりに-1とした。

トークンの分解

トークンの分解は、正規表現とmatch()を使って行なうものとする。もちろんこれではJavaScriptの全ての文法に対応した完璧なトークン化はできないが、限られたコードにだけ対応すればいいので十分なものができると思われる。

ということで、最低限分解できなければいけないトークンとして、以下を想定した。

トークン

正規表現

備考

文字列リテラル

'.*?'

\'のエスケープには対応しない。

".*?"

\"のエスケープには対応しない。

識別子(キーワードや変数)

[$_A-Za-z]+

数字の使用は不可とする。

数値

\d+

整数のみとする。

アロー関数

\)=>

この部分だけは改行できないためひとかたまりで扱う。

その他

[^\(\)\[\]{}$_A-Za-z0-9'"\/]+

+=&&>===など2文字以上の演算子など。

.

それ以外の1文字(カッコ、;,など)。

これらの正規表現を、以下のようにmatch()関数で使用すると分解されたトークンの配列が得られる。

s = 'a=(x)=>{return(x*100);}';
r = /'.*?'|".*?"|[$_A-Za-z]+|\d+|\)=>|[^\(\)\[\]{}$_A-Za-z0-9'"\/]+|./g;
t = s.match(r);
// t = ['a', '=', '(', 'x', ')=>', '{', 'return', '(', 'x', '*', '100', ')', ';', '}']

ここでいくつか注意が必要である。今回簡単のため、正規表現リテラルはトークン化の対象外とした(正規表現リテラルを表す/は、コメントや除算演算子としても使われる記号のため、判別が難しいのである)。つまり、match()に渡す正規表現は、リテラルではなくRegExp()と文字列で表記しなければいけない。

r = RegExp('\'.*?\'|".*?"|[$_A-Za-z]+|\\d+|\\)=>|[^\\(\\)\\[\\]{}$_A-Za-z0-9\'"/]+|.', 'g');

さらに、文字列リテラル中の\'のエスケープ表記も対応しないことにしたため、代わりに\x27などと表記する必要がある。そしてもう一点、長い文字列リテラルを作ると途中での改行ができなくなってしまうため、文字列リテラルは細かく分割して記述しておく。

r = RegExp([
    '\x27.*?\x27',
    '".*?"',
    '[$_A-Za-z]+',
    '\\d+',
    '\\)=>',
    '[^\\(\\)\\[\\]{}$_A-Za-z0-9\x27"/]+',
    '.'
].join('|'), 'g');

トークンによる行埋めのルール

ところで、トークンは2文字以上のものが存在するため、トークンを順番に並べていってもちょうど目的の文字数になるとは限らない。トークンに分解して前述のアスキーアートを作ろうとしても、例えばちょうど38文字の場所で空白を挿入できるとは全く保証されていないのだ。

ここまで特に言及していなかったが、変数名を全て1文字としているのも、なるべく改行や空白挿入の自由度を大きくするためである。また、通常のプログラミングにおいては使用するのが常識となっている変数定義のletconstを一切使っていないのも、文字数や空白の問題があり、今回のQuineプログラミングにとっては逆に有害だからである。

さて、そこで、文字数が合わなかった場合は、以下のルールで適当な範囲コメント/* ~ */を挿入して文字数調整を行なうものとする。

状況

対応

直前のブロックがコメントで終わっていない場合

最終行以外

末尾をコメントで埋めて文字数調整し、次のブロックにコメントを持ち越す。

aaaがコード、cccがコメント。

aaaaaaaaaa/*ccc

最終行

先頭をコメントで埋めて文字数調整する。

※末尾ではなく先頭なのは、著作権表示が右下に来るようにするためである。

/*ccc*/aaaaaaaa

直前のブロックがコメントで終わっている場合

先頭をコメントで埋めて文字数調整する。

ccc*/aaaaaaaaaa

コメントに使用する文字列は何でもよいが、全くランダムな文字列だと、その部分だけ目立ってしまい不自然だったので、周囲のコードになじむようa=0;のような代入文を模した文字列を作成して使用することにした。

ちなみに、乱数を使用してしまうと、実行のたびに結果が異なってしまいQuineにならないので、使用してはいけない。乱数のようなものが必要な場合は、剰余などを使って予測可能なものを擬似的に作り出す必要がある。

// コメント用の適当な文字列の生成
p = (() => {
    g = String.fromCharCode;
    p = '';
    for (i = 0; 1000 > i; i++) {
        p += g(97 + (i * 7) % 26) + '=' + i * 3 % 10 + ';';
    }
    return p;
})();

そして、コメント挿入による文字数調整は、コードにすると以下のようになる。

// 次のブロックの先頭に来るコメント終端
m = '';
// コメント文字列からの切り取り位置
b = 0;
// 各行のコードを生成する関数(nは連続する文字数、kは最終行かどうか)
a = (n, k) => {
    // 前のブロックから持ち越されたコメント終端
    q = m;
    // まず、できるだけ多くのトークンで埋める(コメント持ち越しが無い場合は、末尾コメント用に2文字手前まで)
    while (t[0] && (m ? n : n - 2) >= (q + t[0]).length) {
        q += t.shift();
    }
    // コメントの持ち越しがある場合
    if (m) {
        // 先頭をコメントで埋めて文字数調整する(末尾のコメント持ち越しは無し)
        q = q.padStart(n, p.substring(b, b += n));
        m = '';
    // コメント持ち越しが無い場合
    } else{
        // 次のトークンを加えてちょうどの文字数になる場合
        if (t[0] && (q + t[0]).length == n) {
            q += t.shift();
        // 次の2つのトークンを加えてちょうどの文字数になる場合
        } else if (t[0] && t[1] && (q + t[0] + t[1]).length == n) {
            q += t.shift() + t.shift();
        // ちょうどの文字数にならない場合
        } else {
            // 最終行の場合、先頭にコメントを追加して文字数調整
            if (k) {
                q = '/*' + ('*/' + q).padStart(n - 2, p.substring(b, b += n));
            // それ以外の場合、末尾をコメントで終わらせて文字数調整
            } else {
                q = (q + '/*').padEnd(n, p.substring(b, b += n));
                m = '*/';
            }
        }
    }
    return q;
};

コードの生成と出力

ここまでに用意したアスキーアートの定義と各行のコードを生成する関数を使って、最終的な全体のコードを生成していく処理が以下となる。

// 1行の文字数
w = 77;
// 行内の何文字目か
x = 0;
// 最終的な出力文字列
o = '';
// アスキーアートのデータに沿ってくり返し
for (i = 0; d.length > i; i++) {
    e = d[i];
    // 0または-1(行内の最後のブロック)の場合
    if (0 >= e) {
        // 行末までのコードを生成
        o += a(w - x, 0 > e) + '\n';
        x = 0;
    // 1以上(行内の途中のブロック)の場合
    } else {
      // 指定長さのコードとそれに続く空白を生成
      o += a(e).padEnd(z = e + d[++i]);
      x += z;
    }
}
// 出力
console.log(o);

仕上げ

ここまでのコードをつなぎ合わせれば、「自分自身のコードをトークンに分解して、アスキーアートになるように出力するプログラム」になるはずである。しかし、最後の仕上げに際して以下の注意が必要となる。

  • コメント/* ~ */を除去してからトークン分解を行なうため、コード内に/**/という文字列があってはいけない。コメント挿入関数のところの文字列は'\x2f*''*\x2f'と書き換える。

  • 空白を全て除去してからトークン分解を行なうため、空白が無くなっても問題無いコードにしなければならない。注意すべき点はreturn xelse if ~である。これらはそれぞれreturn(x)else{if ~ }とすることで、空白が無くなっても問題無いコードにできる。

  • 最終的なコードにおいては関係ないが、開発途中のコードでコメントを入れる場合は、//~ではなく/* ~ */を使う必要がある。

上記をふまえて、とりあえず実行できるようになったものが以下となる。

f = function(c) {
    /* トークンの定義 */
    r = RegExp([
        '\x27.*?\x27',
        '".*?"',
        '[$_A-Za-z]+',
        '\\d+',
        '\\)=>',
        '[^\\(\\)\\[\\]{}$_A-Za-z0-9\x27"/]+',
        '.'
    ].join('|'), 'g');
    /* 自分自身のコードをトークンに分解 */
    s = ('f=' + f + ';f("').replace(RegExp([
        '[\\n\x20]',
        '\/\\*[^]*?\\*\/'
    ].join('|'), 'g'), '') + c + '")';
    t = s.match(r);
    /* アスキーアートの定義 */
    d = [
        0, 0, 38, 1, 0, 37, 3, 0, 35, 7, 0,
        34, 9, 0, 32, 13, 0, 30, 7, 3, 7, 0,
        29, 7, 5, 7, 0, 27, 23, 0, 26, 25, 0,
        24, 7, 15, 7, 0, 0, 0, -1
    ];
    /* コメント用の適当な文字列 */
    p = (() => {
        g = String.fromCharCode;
        p = '';
        for (i = 0; 1000 > i; i++) {
            p += g(97 + (i * 7) % 26) + '=' + i * 3 % 10 + ';';
        }
        return (p);
    })();
    /* コメント挿入関数 */
    m = '';
    b = 0;
    a = (n, k) => {
        q = m;
        while (t[0] && (m ? n : n - 2) >= (q + t[0]).length) {
            q += t.shift();
        }
        if (m) {
            q = q.padStart(n, p.substring(b, b += n));
            m = '';
        } else{
            if (t[0] && (q + t[0]).length == n) {
                q += t.shift();
            } else {
                if (t[0] && t[1] && (q + t[0] + t[1]).length == n) {
                    q += t.shift() + t.shift();
                } else {
                    if (k) {
                        q = '\x2f*' + ('*\x2f' + q).padStart(n - 2, p.substring(b, b += n));
                    } else {
                        q = (q + '\x2f*').padEnd(n, p.substring(b, b += n));
                        m = '*\x2f';
                    }
                }
            }
        }
        return (q);
    };
    /* コードの出力 */
    w = 77;
    x = 0;
    o = '';
    for (i = 0; d.length > i; i++) {
        e = d[i];
        if (0 >= e) {
            o += a(w - x, 0 > e) + '\n';
            x = 0;
        } else {
            o += a(e).padEnd(z = e + d[++i]);
            x += z;
        }
    }
    console.log(o);
};
f("(C)2021 Azicore https://azisava.sakura.ne.jp/");

上記のコードの出力は以下となる。コードを整形する処理を内包しているため、実行前に自分自身で手作業でアスキーアートに整形する必要はないというのがポイントである。

f=function(c){r=RegExp(['\x27.*?\x27','".*?"','[$_A-Za-z]+','\\d+','\\)=>',/*
*/'[^\\(\\)\\[\\]{}$_A-Za-z0-9\x27"/]+','.'].join('|'),'g');s=('f='+f+';f("')
.replace(RegExp(['[\\n\x20]',/*4;n=7;u */'\/\\*[^]*?\\*\/'].join('|'),'g'),''
)+c+'")';t=s.match(r);d=[0,0,38,1,0/*   */,37,3,0,35,7,0,34,9,0,32,13,0,30,7,
3,7,0,29,7,5,7,0,27,23,0,26,25,0,24       ,7,15,7,0,0,0,-1];p=(()=>{g=String.
fromCharCode;p='';for(i=0;1000>i;i         ++){p+=g(97+(i*7)%26)+'='+i*3%10/*
*/+';';}return(p);})();m='';b=0;             a=(n,k)=>{q=m;while(t[0]&&(m?n:n
-2)>=(q+t[0]).length){q+=t./*6       y*/       shift();}if(m){q=q.padStart(n,
p.substring(b,b+=n));m='';}/*       c=4*/       else{if(t[0]&&(q+t[0]).length
==n){q+=t.shift();}else{if(                       t[0]&&t[1]&&(q+t[0]+t[1])/*
*/.length==n){q+=t.shift()                         +t.shift();}else{if(k){q/*
t*/='\x2f*'+('*\x2f'+q).       padStart(n-2,p.       substring(b,b+=n));}else
{q=(q+'\x2f*').padEnd(n,p.substring(b,b+=n));m='*\x2f';}}}}return(q);};w=77;x
=0;o='';for(i=0;d.length>i;i++){e=d[i];if(0>=e){o+=a(w-x,0>e)+'\n';x=0;}else{
/*j=5;q=8;x=1;e=4;l=7;*/o+=a(e).padEnd(z=e+d[++i]);x+=z;}}console.log(o);};f(

一応はエラーも無く実行することができ、このようにアスキーアート自体は想定通り生成されている。しかし、よく見ると、最後はコードが途中で切れてしまっている(つまりこのコードはもう一度実行することはできないので、まだQuineになっていない)し、文字数調整のコメントもかなり多くの箇所に挿入されてしまっていて見栄えが悪い(Aの中央の島の部分に至っては全てコメントになってしまっている)。

実は、ここから先は非常に泥臭い作業が必要であった。空白や改行が挿入される位置がもっと良い感じになって文字数調整のコメントがなるべく少なくなり、コードが全てきっちり収まるように、

  • 処理に影響の無い範囲でコードの順番を変えてみたり…

  • ;-0などあっても無くても変わらないものを1文字付け足してみたり減らしてみたり…

  • =====にして1文字増やしてみたり…

  • 長い文字列リテラルを分割して表記してみたり…

…といったような微調整をいろいろ試してみて、なんとか納得できる形になったのが、最終的なコードである。

f=function(c){r=RegExp(['\x27.*?\x27','".*?"','[$_A-Za-z]+','\\d+','\\)=>',/*
*/'[^\\(\\)\\[\\]{}$_A-Za-z0-9\x27"\/]+','.'].join('|'),'g');s=('f='+f+';f("'
).replace(RegExp(['[\\n\x20]','\/\\*'+ '[^]*?'+'\\*\/'].join('|'),'g'),'')+c+
'")';d=[0,0,38,1,0,37,3,0,35,7,0,34,9   ,0,32,13,0,30,7,3,7,0,29,7,5,7,0,27/*
*/,23,0,26,25,0,24,7,15,7,0,0,0,-1]       ;p=(()=>{g=String.fromCharCode;p=''
;for(i=0;1000>i;i++){p+=g(97+(i*7)         %26)+'='+i*3%10+';'}return(p)})();
t=s.match(r);m='';b=0;a=(n,k)=>{             q=m;while(t[0]&&(m?n-0:n-2)>=(q+
t[0]).length){q+=t.shift()}if(       m){       q=q.padStart(n,p.substring(b,b
+=n));m=''}else{if(t[0]&&(q+t       [0]).       length===n){q+=t.shift()}else
{if(t[0]&&t[1]&&(q+t[0]+t[1                       ]).length===n){q+=t.shift()
+t.shift()}else{if(k){q=/*                         z=9;*/'\x2f*'+('*\x2f'+q).
padStart(n-2,p.substring       (b,b+=n))}else{       q=(q+'\x2f*').padEnd(n,p
.substring(b,b+=n));m='*\x2f'}}}}return(q)};w=77;x=0;o='';for(i=0;d.length>i;
i++){e=d[i];if(0>=e){o+=a(w-x,0>e)+'\n';x=0}else{o+=a(e).padEnd(z=e+d[++i]);x
/*7;*/+=z}}console.log(o)};f("(C)2021 Azicore https://azisava.sakura.ne.jp/")

文字数調整のコメントは4ヶ所だけに抑えることができ、最後の著作権表示まで無事に全てのコードが収まった。中で行なわれている処理の本質は全く同じなので、あとは見比べていただきたい。本当に処理の本質と関係ない1文字単位の泥臭い修正が行なわれているだけである。

長い文字列リテラルや識別子(StringfromCharCodelengthpadStartsubstringなど)をいかにうまく収めるかがなかなか大変であった。この完成形に至ったのは半ば偶然によるところも大きい。なお、メソッド・プロパティ名であれば、非常に泥臭くなるが、String['from'+'Char'+'Code']のように文字列で表記してもう少し小さなトークンにすることも可能ではある。

ちなみに、なぜ最初から77文字×15行にほぼぴったり収まる長さのコードなのか?という点についてはほとんど触れなかったが、それも泥臭く微調整をくり返した結果である。例えば、「コメント用の適当な文字列を生成する関数」などに疑問を感じた方もいるかと思うが、こういったあまり意味のない処理は文字数調整のために入れたと言っても過言ではない。

おわりに

ここまで読んで下さった方は分かっておられると思うが、Quineには実用上の要素はほとんど無く、プログラミング上の娯楽・アートである。必要無いと言われればそれまでであるが、私はこういうのを楽しめる心と頭の余裕を常に持ち続けていたい(とはいえ、こんなのを何個も作った経験があるわけではない)。

もしこの記事や私のサイト、あるいはこんな私自身に少しでも興味を持っていただけたのであれば、Twitter(@Azicore)でお気軽に絡んでいただけると幸いである。