5 種の記号だけで JavaScript(w/ pipeline operator) を書く

JavaScript は 6 種の記号だけで処理を書ける言語であることは知られており、JSFuckがその実装として有名だ。 Xchars.js本格JavaScript記号プログラミング 6種類の記号だけでJavaScriptを書こう - Qiita もこの理由を学ぶ上で参考になる。
しかし、[]()!+[]()=+ などのバリエーションはあれど 6 種よりも少ない種類の記号で書く方法は今まで見つかっていない。(DOM を使えば可能らしいが。)

もし、現在提案中のパイプライン演算子 が正式に採用されれば、[]|>+ という記号 5 種の組み合わせで書けるようになる。その「トランスパイラ」やアイデアは Xchars.js5文字で書くJavaScript(スライド) で触れられている。

この記事は、本格JavaScript記号プログラミング 6種類の記号だけでJavaScriptを書こう - Qiita の説明手順を参考にし、 5 種の記号で JavaScript のコードを書くための要素についてまとめている。

TL;DR

関数実行を担っていた ()|> で置き換え、 truefalse を得るための !=> で置き換える。

Pipeline operator について

ES Nextのパイプライン演算子ってどうなの? - QiitaJavaScriptのpipeline operatorについてまとめてみた - Qiita が詳しい。 Minimal Proposal、Smart Pipelines、F# Pipelines と 3 種類ある。

function f(x) { return x * x; }
function g(x) { return x % 10; }

// 従来
g(f(1))
// pipeline operator
1 |> f |> g

// minimal
1 |> (x => x + 1) |> f
// smart (topic style then bare style)
1 |> # + 1 |> f
// F#
1 |> x => x + 1 |> f

準備

(2020 年 7 月 19 日現在)ブラウザのコンソールでそのまま動かすことはできない[1]ので、Babel でトランスパイルして動かす。 Babel REPL という便利なものがあるのでそれを利用する。(5 種記号コード本体では使えない書き方ではあるが)console.debug を駆使すれば式の評価値が得られる。 左のメニューの “PRESETS > OPTIONS > Pipeline proposal” の “Minimal” をクリックすれば他の書き方に変えられる。
どれが最終版として取り込まれるかわからないが、この記事では “Minimal” のパターンを載せる。なお、現在の “Smart” では topic style で書かざるを得ないため # の利用を強制され、今回の目標の達成は不可能になる。

前提知識

JavaScript では暗黙の型変換が起きることで有名で、真面目なコードを書くときにも問題になる。この記事で説明するような記号プログラミングでは、この(今となっては扱いにくい)不思議な仕様をフル活用する。
この辺りは特に目新しいことはないため、説明は先人が書いた記事に託したい。JavaScriptのプリミティブへの変換を完全に理解する - Qiita が参考になる。

基本の記号 +[]

ここは従来の 6 種の記号プログラミングの解説と全く同じとなるため飛ばしていく。

数字を得る

記号 []+ と暗黙の型変換(以後コード内では “type-conversion”)により、まず数字が得られる。+[]0 を利用する。また、[a][0] とすることで、(a) のように括弧で括る効果もある。((a+2)*3[a+2][0]*3

+[] // 0  by type-conversion
[+[]][+[]] // [0][0] -> 0 by (type-conversion &) index-access
++[+[]][+[]] // ++[0][0] -> 1 by (type-conversion & index-access &) pre-increment
++[++[+[]][+[]]][+[]]
// ++[1][0] -> 2 by (type-conversion & index-access &) pre-increment x2

得られた数値: 0 1 2 3 4 5 6 7 8 9 10

今後、生み出せた値は表記を簡単にするため(執筆をサボるため)注釈なく使うことがある。また、優先順位のための () も使う。

文字列操作

[]+[] では ""2 + []では "2" と、 hoge + []hoge が文字列になることを利用する。また、文字列化と配列アクセスを使い文字を得えられるし、文字列の結合で(得られている文字のセットの範囲内で)いくらでもつなげられる。

[]+[] // ""  by type-conversion
0+[] // "0" by type-conversion
1+[] // "1"
"1"+"0" // "10"
+"10" // 10 by type-conversion
"35"[1] // "5" by index-access

JavaScript には undefinedNaN なども存在するが、これらを暗黙の型変換することで様々な文字を得られる。

[][[]] // undefined  by (wrong)property-access
undefined+[] // "undefined" by type-conversion
"undefined"[0] // "u"
"undefined"[3] // "e"
+"11e100" // 1.1e+101 by type-conversion
+"11e100"+[] // "1.1e+101" by type-conversion
"1.1e+101"[1] // "."
"1.1e+101"[4] // "+"
+"0.0000001" // 1e-7 by type-conversion
((1e-7)+[])[0] // "-"
+"1e309" // Infinity by type-conversion
+undefined // NaN by type-conversion

得られた文字: 0123456789adefintuyIN+-. (数値は文字と相互に交換可能なので以後省略)

さらに、関数の文字列化も活用する。

[]["find"] // Array.prototype.find  by property-access
[]["find"]+[] // "function find() { [native code] }" by type-conversion

得られた文字(スペース含む): 0123456789acdefinotuvyIN+-. (){}[]cov (){}[]が新規)

ここまでが []+ だけで得られる文字の現在考えられている限界となる。

記号 > を投入

ここで、比較演算と関数呼び出しに使える記号 > を投入する。ここから従来の説明と少し離れはじめる。

[]>[] // 0>0 -> false  by type-conversion
1>[] // 1>0 -> true by type-conversion

得られた文字(スペース含む): 0123456789acdefilnorstuvyIN+-. (){}[]lrsが新規)

ここまでに得られた文字を組み合わせると、constructor という文字列を組み立てられる。値と constructor によるプリミティブラッパーオブジェクトの取得により、文字や Function を得る。(Function は重要)

0["constructor"] // Number
0["constructor"]+[] // "function Number() { [native code] }"
""["constructor"] // String
""["constructor"]+[] // "function String() { [native code] }"
false["constructor"] // Boolean
false["constructor"]+[] // "function Boolean() { [native code] }"
[]["constructor"] // Array
[]["constructor"]+[] // "function Array() { [native code] }"
[]["find"]["constructor"] // Function
[]["find"]["constructor"]+[] // "function Function() { [native code] }"

得られた文字(スペース含む): 0123456789abcdefgilmnorstuvyABFINS+-. (){}[]bgmABFSが新規)

最後の記号 |

ここで pipeline operator 呼び出しのため、お待ちかねの | を投入する。number#toString が 2~36 進数に対応し、36 進数では 1 桁を 0z を使い表現することを利用する。

36|>0["toString"] // "0"
36|>10["toString"] // "a"
36|>35["toString"] // "z"
36|>36["toString"] // "10"

得られた文字(スペース含む): 0123456789abcdefghijklmnopqrstuvwxyzABFINS+-. (){}[]hjkpqwxzが新規)

ここで一つ eval を取得してみる。Function を利用する方法と、関数呼び出し時に余剰なパラメタ [] を渡しつつ実行するテクニックを用いる。今後もこれを多用することになる。
文字がそろえば任意のグローバルオブジェクトを取得できる。

// Function("return eval")([])
[]|>("return eval"|>Function) // eval by pipeline operator (minimal/F#)

// []|>("return eval"|>[]["find"]["constructor"])
// []|>["return eval"|>[]["find"]["constructor"]][0]

String.fromCharCode への道のり

あとは 本格JavaScript記号プログラミング 6種類の記号だけでJavaScriptを書こう - Qiita と全く同じように、足りない大文字を獲得し、任意の文字を取得する String.fromCharCode を使えるようにして Function で動かす。

P

まず P を Promise から取得する。

[]|>("return async function(){}"|>Function) // async function(){}
([]|>async function(){})+[] // "[object Promise]" by type-conversion

// ([]|>([]|>("return async function(){}"|>Function)))+[]
// ([]|>([]|>("return async function(){}"|>[]["find"]["constructor"])))+[]
// [[]|>[[]|>["return async function(){}"|>[]["find"]["constructor"]][0]][0]][0]+[]

得られた文字(スペース含む): 0123456789abcdefghijklmnopqrstuvwxyzABFINPS+-. (){}[]Pが新規)

C

次に C をプロパティ名から取得する。sort は関数か undefined しか受け付けないため呼び出しが回りくどい。

([]|>[]["entries"])["constructor"] // Object
([]|>[]["entries"])["constructor"]+[] // "function Object() { [native code] }"
String|>Object["getOwnPropertyNames"]
// ["length", "name", "prototype", "fromCharCode", "fromCodePoint", "raw"]
undefined|>(String|>Object["getOwnPropertyNames"])["sort"]
// ["fromCharCode", "fromCodePoint", "length", "name", "prototype", "raw"]
(undefined|>(String|>Object["getOwnPropertyNames"])["sort"])[0] // "fromCharCode"

得られた文字(スペース含む): 0123456789abcdefghijklmnopqrstuvwxyzABCFINOPS+-. (){}[]COが新規)

任意の文字

あとは String.fromCharCode と数字を使って文字を作ればよい。

124|>String["fromCharCode"] // "|"
62|>String["fromCharCode"] // ">"
128519|>String["fromCharCode"] // "😇"
129322|>String["fromCharCode"] // "🤪"

得られた文字:(JavaScript が扱える)任意の文字

実行例

Function に任意の文字を与えて呼び出すと、任意のコードを再現し実行できる。(eval より手っ取り早い)

function f(n) { return n <= 1 ? n : f(n - 1) + f(n - 2); }
console.error(f(6));
console.debug(`Goodbye ${f(7)}`);

「トランスパイル」途中結果

[]|>(
"function f(n) { return n <= 1 ? n : f(n - 1) + f(n - 2); }"+
"console.error(f(6));"+
"console.debug(`Goodbye ${f(7)}`);"
|>[]["find"]["constructor"])

まとめ

JavaScript の暗黙の型変換は正気でないので、おとなしくこれからも TypeScript を使おうとおもう。


  1. tc39/proposal-pipeline-operator によれば、Firefox を --enable-pipeline-operator 付きでビルドすればできる模様。 ↩︎