なんやねんこれ

JavaScriptで記号のみを用いてできるだけ短く英数字を出力する + 記号のみを用いてFizzBuzzをやってみるが少し伸びてたので、もうちょっとまともに解説しようと思ったのが記事のきっかけです。

そもそも記号プログラミングとは

JavaScriptを記号だけで書こうという宗教プログラミングです。
得られるものとしては

  • 読解可能な難読化コードの手動生成技術
  • 冗長化された素晴らしいコード
  • 謎の感動
  • 狂気

と言ったものが挙げられます。

制約

具体的なルールは以下の通りです。

0x20 - 0x21
  !
0x22 - 0x26
" # $ % &
0x27 - 0x2f
' ( ) * + , - . /
0x3a - 0x40
: ; < = > ? @
0x5b - 0x60
[ \ ] ^ _ `
0x7b - 0x7e
{ | } ~

テクニック

では早速記号プログラミングを始めましょう!

  1. about:blankを開き、
  2. Ctrl + Shift + K (Command + Option + K) を押下し、
  3. コンソールにフォーカスが入れば準備完了!

基礎編

記号プログラミングでは[]が多用されます。これは配列リテラルと呼ばれるもので、非常に活用しやすいのが特徴です。使用例を以下に記述します。

Array
[] // Array []
Boolean
 ![] // false
!![] // true
undefined
[][[]] // undefined
Number
    +[] // 0
    -[] // -0
    ~[] // -1
   -~[] // 1
  ~-~[] // -2
 -~-~[] // 2
  []/[] // NaN
 ~[]/[] // -Infinity
-~[]/[] // Infinity
String
  []+[] // ""
 ![]+[] // "false"
!![]+[] // "true"

ね?簡単でしょう?JavaScriptは弱い動的型付け言語なので、このような様々な表記で多様な値に様変わりします。
また、任意の値に配列を加算するような書き方をすると、その値はString型になります。これは全てのString型がString型の配列とも解釈できることに起因するものです。

基本編

では、入門編で生成したオブジェクトから様々な文字列を獲得してみましょう。

false
 ![]+[]          // "false"
(![]+[])[+[]]        // "f"
(![]+[])[-~[]]       // "a"
(![]+[])[-~-~[]]     // "l"
(![]+[])[-~-~-~[]]   // "s"
(![]+[])[-~-~-~-~[]] // "e"
true
 !![]+[]         // "true"
(!![]+[])[+[]]      // "t"
(!![]+[])[-~[]]     // "r"
(!![]+[])[-~-~[]]   // "u"
(!![]+[])[-~-~-~[]] // "e"
undefined
 [][[]]+[]              // "undefined"
([][[]]+[])[+[]]                // "u"
([][[]]+[])[-~[]]               // "n"
([][[]]+[])[-~-~[]]             // "d"
([][[]]+[])[-~-~-~[]]           // "e"
([][[]]+[])[-~-~-~-~[]]         // "f"
([][[]]+[])[-~-~-~-~-~[]]       // "i"
([][[]]+[])[-~-~-~-~-~-~[]]     // "n"
([][[]]+[])[-~-~-~-~-~-~-~[]]   // "e"
([][[]]+[])[-~-~-~-~-~-~-~-~[]] // "d"
NaN
 []/[]+[]        // "NaN"
([]/[]+[])[+[]]    // "N"
([]/[]+[])[-~[]]   // "a"
([]/[]+[])[-~-~[]] // "N"
Infinity
 -~[]/[]+[]             // "Infinity"
(-~[]/[]+[])[+[]]              // "I"
(-~[]/[]+[])[-~[]]             // "n"
(-~[]/[]+[])[-~-~[]]           // "f"
(-~[]/[]+[])[-~-~-~[]]         // "i"
(-~[]/[]+[])[-~-~-~-~[]]       // "n"
(-~[]/[]+[])[-~-~-~-~-~[]]     // "i"
(-~[]/[]+[])[-~-~-~-~-~-~[]]   // "t"
(-~[]/[]+[])[-~-~-~-~-~-~-~[]] // "y"

そして空のオブジェクトからこんなものも採取できます。

[object Object]
/* Don't write "{}+[]". It's not working! */
 ({})+[]             // "[object Object]"
(({})+[])[+[]]                     // "["
(({})+[])[-~[]]                    // "o"
(({})+[])[-~-~[]]                  // "b"
(({})+[])[-~-~-~[]]                // "j"
(({})+[])[-~-~-~-~[]]              // "e"
(({})+[])[-~-~-~-~-~[]]            // "c"
(({})+[])[-~-~-~-~-~-~[]]          // "t"
(({})+[])[-~-~-~-~-~-~-~[]]        // " "
(({})+[])[-~-~-~-~-~-~-~-~[]]      // "O"
(({})+[])[-~-~-~[]*-~-~-~[]]       // "b"
(({})+[])[-~-~-~-~-~[]*-~-~[]]     // "j"
(({})+[])[(-~-~-~[]<<-~-~[])+~[]]  // "e"
(({})+[])[-~-~-~[]<<-~-~[]]        // "c"
(({})+[])[(-~-~-~[]<<-~-~[])-~[]]  // "t"
(({})+[])[-~-~-~-~-~-~-~[]*-~-~[]] // "]"

さらにビルトイン関数を呼び出しましょう。Firefoxでは全てこのような出力になります。

native code
$+[]
/*
"function () {
    [native code]
}"
 */
$$+[]
/*
"function () {
    [native code]
}"
 */
$_+[]
/*
"function () {
    [native code]
}"
 */

長すぎるので文字列の分解は省略しますが、この文字列には改行文字も含まれることを忘れないようにしましょう。また、jQueryでは$が上書きされるため、下2つの内何れかの表記する必要があります。

応用編

基本編の段階ではまだまだ扱える文字が少ないので、この限られた文字から様々なオブジェクトを生成しましょう。
幸い現時点で次の文字を並べることができます。

'c' // "[object Object]"
 +  // |~~~~~^ [5]
'o' // "[object Object]"
 +  // |~^ [1]
'n' // "function () {\n    [native code]\n}"
 +  // |~~^ [2]
's' // "false"
 +  // |~~~^ [3]
't' // "true"
 +  // |^ [0]
'r' // "true"
 +  // |~^ [1]
'u' // "undefined"
 +  // |^ [0]
'c' // "[object Object]"
 +  // |~~~~~^ [5]
't' // "true"
 +  // |^ [0]
'o' // "[object Object]"
 +  // |~^ [1]
'r' // "true"
    // |~^ [1]

ここでなぜ'constructor'などという文字列を生成しているかというと、次のようなことができるようになるからです。

[]['constructor']
/*
"function Array() {
    [native code]
}"
 */
(![])['constructor']+[]
/*
"function Boolean() {
    [native code]
}"
 */
(+[])['constructor']+[]
/*
"function Number() {
    [native code]
}"
 */
([]+[])['constructor']+[]
/*
"function String() {
    [native code]
}"
 */
({})['constructor']+[]
/*
"function Object() {
    [native code]
}"
 */
/ /['constructor']+[]
/*
"function RegExp() {
    [native code]
}"
 */
[]['constructor']['constructor']
/*
"function Function() {
    [native code]
}"
 */

なんということでしょう、先程まで数少なかった文字列のレパートリーが、constructorの手によってこんなにも増えました。
(正規表現の部分、ハイライトがおかしいですが、正しく動作します。)

この奇跡は記号に直すとこうなります。

/*c*/ (({})+[])[-~-~-~-~-~[]]
  +
/*o*/ (({})+[])[-~[]]
  +
/*n*/ ($+[])[-~-~[]]
  +
/*s*/ (![]+[])[-~-~-~[]]
  +
/*t*/ (!![]+[])[+[]]
  +
/*r*/ (!![]+[])[-~[]]
  +
/*u*/ ([][[]]+[])[+[]]
  +
/*c*/ (({})+[])[-~-~-~-~-~[]]
  +
/*t*/ (!![]+[])[+[]]
  +
/*o*/ (({})+[])[-~[]]
  +
/*r*/ (!![]+[])[-~[]]

つまり、このように書けば

(
  []
  [
    (({})+[])[-~-~-~-~-~[]]+
    (({})+[])[-~[]]+
    ($+[])[-~-~[]]+
    (![]+[])[-~-~-~[]]+
    (!![]+[])[+[]]+
    (!![]+[])[-~[]]+
    ([][[]]+[])[+[]]+
    (({})+[])[-~-~-~-~-~[]]+
    (!![]+[])[+[]]+
    (({})+[])[-~[]]+
    (!![]+[])[-~[]]
  ]+
  []
)
[-~-~-~[]*-~-~-~[]] // "A"

といった文字が使えるようになるわけです。JavaScriptの奇跡に圧倒的感謝:pray::pray::pray:

発展編

まだまだ扱える文字は不完全です。この章では応用編でさえも獲得できなかった文字列をすべて埋めていきます。

JavaScriptでは、次のようなコードでUnicode文字コードから任意のアルファベットなどを生成できます。

'\u0068' // "h"

これを関数化し

(() => '\u0068')() // "h"

こうすると

new Function('return "\u0068"')() // "h"

こうなり

Function['constructor']('return "\u0068"')()

さらにこうなり

[]['constructor']['constructor']('return "\u0068"')()

最後にこうなります。
この時点で全ての記号でない部分が文字列になりました。
さて、お待ちかねの記号化のお時間です。

[]
[
  (({})+[])[-~-~-~-~-~[]]+ // "c"
  (({})+[])[-~[]]+         // "o"
  ($+[])[-~-~[]]+          // "n"
  (![]+[])[-~-~-~[]]+      // "s"
  (!![]+[])[+[]]+          // "t"
  (!![]+[])[-~[]]+         // "r"
  ([][[]]+[])[+[]]+        // "u"
  (({})+[])[-~-~-~-~-~[]]+ // "c"
  (!![]+[])[+[]]+          // "t"
  (({})+[])[-~[]]+         // "o"
  (!![]+[])[-~[]]          // "r"
]
[
  (({})+[])[-~-~-~-~-~[]]+ // "c"
  (({})+[])[-~[]]+         // "o"
  ($+[])[-~-~[]]+          // "n"
  (![]+[])[-~-~-~[]]+      // "s"
  (!![]+[])[+[]]+          // "t"
  (!![]+[])[-~[]]+         // "r"
  ([][[]]+[])[+[]]+        // "u"
  (({})+[])[-~-~-~-~-~[]]+ // "c"
  (!![]+[])[+[]]+          // "t"
  (({})+[])[-~[]]+         // "o"
  (!![]+[])[-~[]]          // "r"
]
(
  (!![]+[])[-~[]]+     // "r"
  (!![]+[])[-~-~-~[]]+ // "e"
  (!![]+[])[+[]]+      // "t"
  ([][[]]+[])[+[]]+    // "u"
  (!![]+[])[-~[]]+     // "r"
  ([][[]]+[])[-~[]]+   // "n"
  (/ /+[])[-~[]]+      // " "
  (/"/+[])[-~[]]+      // "\""
  (/\\/+[])[-~[]]+     // "\\"
  ([][[]]+[])[+[]]+    // "u"
  -[]+                 // "0"
  -[]+                 // "0"
  -~-~-~-~-~-~[]+      // "6"
  -~-~-~-~-~-~-~-~[]+  // "8"
  (/"/+[])[-~[]]       // "\""
)
() // "h"

これで全ての文字をさんしょうすることがでk

まとめ

記号プログラミングはいいぞ。