やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

演算子オーバーロードする

 ライブラリを使用して実現できましたが、実用性皆無です。

成果物

See the Pen Untitled by ytyaru (@ytyaru) on CodePen.

演算子オーバーロードとは

 演算子を使った計算式をフックして好きな結果を返すことです。残念ながらJavaScriptでは不可能です。

 配列に差分を渡したい時、JSだと次のように書けます。

let a = [1,2]
a = [...a, ...[3,4]]
console.log(a) // [1,2,3,4]

 でも冗長です。理想は以下です。

let a = [1,2]
a += [3,4]
console.log(a) // [1,2,3,4]

 これは動作しません。でも、こう動作するように実装したいのです。

 このとき+=という演算子を使っています。この+=演算子が使われたときの処理を好きに定義するのが演算子オーバーロードです。理想は次のような実装です。

Array.prototype.__plusEqual(v) { return [...this, ...v] }

 これは動作しません。定義はされますが、+=実行時に呼ばれません。

調査

 ググったら以下記事がヒットしました。

記事 感想
黒魔術で無理やり演算子オーバーロードを実装して遊ぶ 加算代入が表現できない
JavaScriptで演算子オーバーロードしてみる(BabelでAST) 構文木から解析するのは大変すぎる。依存解決でトラブりそう
operator-overloading-js これならイケる?

 他、valueOfdefinePropertyを使って演算子オーバーロード風に書けないか挑戦した記事もありますが、実装できません。使用された演算子が取得できないからです。それが可能なAPIはありません。

 今回はoperator-overloading-jsライブラリを使用して、演算子オーバーロードを実現しました。以下、その作業履歴です。

operator-overloading-js

仕様理解

 ドキュメントを読むと、次のようような唯一のAPIがあるようです。

overload(function(){ /* 演算子オーバーロードしたコードを書くと解析して実行する */ })()

 今回やりたい配列の+=演算子オーバーロードは、以下のように書くと実装できそうです。この__addAssignという名前は演算子ごとに定義されているようです。全40種類の演算子に対応しており、詳しくはドキュメントを参照。

Array.prototype.__addAssign = function(v){
    return [...this, ...v]
}

 最初のコード例で説明すると、a += [3, 4]のとき実行されます。[3,4]__addAssign関数の引数vに代入されます。thisは配列自身[1,2]です。代入される値はreturnで返した値です。つまり配列自身[1,2]と渡された[3,4]を展開して結合した新しい配列[1,2,3,4]を返します。

インストール

 operator-overloading-jsをインストールします。

 ブラウザで直接使いたいです。Node.js用インストールnpmと、ブラウザ用ファイルのインストールbowerがありました。今回はブラウザで使いたいのでBowerを試してみましょう。

 BowerはNode.jsのパッケージです。ブラウザ用ライブラリのパッケージ管理ツールです。紛らわしいですね。Node.jsという実行環境がもつnpmというNode.js用パッケージ管理ツールを使って、bowerというブラウザ用パッケージ管理ツールをインストールし、それを使って、operator-overloading-jsをインストールするのです。

 ネストが深すぎて意味不明です。これだからNode.jsは嫌だ。

Bower

  1. インストールする
    1. Node.js
    2. Bower
    3. operator-overloading-js
  2. 参照する
  3. 実行する

インストールする

 まずNode.jsのインストールです。環境や時代によって方法が変わりします。私は昔、次のようにインストールしました。以降で使うnpmコマンドも含まれます。

 Bowerをインストールします。

npm install bower

 bowerの実行ファイルは以下パスに存在しました。

./bower/node_modules/.bin/bower

 bowerをフルパスだと仮定してコマンド表記します。(今回はグローバルインストールしてないからフルパス指定する必要あり)

bower install operator-overloading

 operator-overloading-jsがインストールされたはずです。どこにあるのか表示させましょう。

$ bower list --paths

  'operator-overloading': 'bower_components/operator-overloading/dist/browser/overload.min.js'

参照する

 operator-overloading-jsのJSライブラリを参照します。

 先述の通り以下パスにインストールしたoperator-overloading-jsがありました。

./bower_components/operator-overloading/

 さらに掘り下げると、以下パスに目的のJSライブラリファイルが存在しました。

./dist/browser/overload.min.js
./dist/browser/overload.js

 軽量化されたoverload.min.jsのほうを使いたいですね。このファイルを実行するにはHTMLで参照する必要があります。とり急ぎ上記ライブラリと同じ場所に作るなら以下のようになります。

<script src="overload.min.js"></script>

実行する

 早速ライブラリを使おう! と思ったらバグりました。修正してから実行します。

  1. バグ修正
    1. min が文字化けして使えない
    2. 非min のコードから対象箇所を参照しコピペする
  2. 実行する
    1. operator-overloading-jsAPIであるoverload()関数が存在するか確認する
    2. 配列に+=したときの処理をオーバーロードしてみる

バグ修正

 残念ながら最初からバグに遭遇しました。

min が文字化けして使えない

 operator-overloading-jsAPIであるoverload()関数が存在するか確認します。

vim index.html
<script src="overload.min.js"></script>
<script>
console.log(overload)
</script>

 ブラウザの開発者ツールにあるコンソールで結果を表示します。すると残念ながら以下エラーになりました。

Uncaught SyntaxError: Invalid regular expression: /[ªµºÀ-ÖØ-öø-ˈ-Ë‘Ë -ˤˬˮͰ-ʹͶͷͺ-ͽΆΈ-ΊΌΎ-ΡΣ-ϵϷ-ÒÒŠ-Ô§Ô±-Õ–Õ™Õ¡-Ö‡×-תװ-ײؠ-يٮٯٱ-Û“Û•Û¥Û¦Û®Û¯Ûº-Û¼Û¿ÜÜ’-ܯÝ-ޥޱߊ-ߪߴߵߺࠀ-ࠕࠚࠤࠨࡀ-ࡘࢠࢢ-ࢬऄ-हऽà¥à¥˜-ॡॱ-ॷॹ-ॿঅ-ঌà¦à¦à¦“-নপ-রলশ-হঽৎড়à§à§Ÿ-ৡৰৱਅ-ਊà¨à¨à¨“-ਨਪ-ਰਲਲ਼ਵਸ਼ਸਹਖ਼-ੜਫ਼ੲ-ੴઅ-àªàª-ઑઓ-નપ-રલળવ-હઽà«à« à«¡à¬…-ଌà¬à¬à¬“-ନପ-ରଲଳଵ-ହଽଡ଼à­à­Ÿ-ୡୱஃஅ-ஊஎ-à®à®’-கஙசஜஞடணதந-பம-ஹà¯à°…-ఌఎ-à°à°’-నప-ళవ-హఽౘౙౠౡಅ-ಌಎ-à²à²’-ನಪ-ಳವ-ಹಽೞೠೡೱೲഅ-ഌഎ-à´à´’-ഺഽൎൠൡൺ-ൿඅ-ඖක-නඳ-රලව-à·†à¸-ะาำเ-ๆàºàº‚ຄງຈຊàºàº”-ທນ-ຟມ-ຣລວສຫອ-ະາຳຽເ-ໄໆໜ-ໟༀཀ-ཇཉ-ཬྈ-ྌက-ဪဿá-á•áš-áá¡á¥á¦á®-á°áµ-á‚á‚Žá‚ -ჅჇáƒáƒ-ჺჼ-ቈቊ-á‰á‰-ቖቘቚ-á‰á‰ -ኈኊ-áŠáŠ-ኰኲ-ኵኸ-ኾዀዂ-ዅወ-ዖዘ-áŒáŒ’-ጕጘ-ášáŽ€-áŽáŽ -á´á-ᙬᙯ-ᙿáš-áššáš -ᛪᛮ-ᛰᜀ-ᜌᜎ-ᜑᜠ-ᜱá€-á‘á -á¬á®-á°áž€-ឳៗៜᠠ-ᡷᢀ-ᢨᢪᢰ-ᣵᤀ-ᤜá¥-ᥭᥰ-ᥴᦀ-ᦫá§-ᧇᨀ-ᨖᨠ-ᩔᪧᬅ-ᬳᭅ-ᭋᮃ-ᮠᮮᮯᮺ-ᯥᰀ-á°£á±-á±á±š-ᱽᳩ-ᳬᳮ-ᳱᳵᳶᴀ-ᶿḀ-ἕἘ-á¼á¼ -ὅὈ-á½á½-ὗὙὛá½á½Ÿ-ώᾀ-ᾴᾶ-ᾼιῂ-ῄῆ-á¿Œá¿-á¿“á¿–-Ίῠ-Ῥῲ-ῴῶ-ῼâ±â¿â‚-ₜℂℇℊ-â„“â„•â„™-â„ℤΩℨK-ℭℯ-ℹℼ-â„¿â……-ⅉⅎⅠ-ↈⰀ-Ⱞⰰ-ⱞⱠ-ⳤⳫ-ⳮⳲⳳⴀ-ⴥⴧⴭⴰ-ⵧⵯⶀ-ⶖⶠ-ⶦⶨ-ⶮⶰ-ⶶⶸ-ⶾⷀ-ⷆⷈ-â·Žâ·-â·–â·˜-ⷞⸯ々-〇〡-〩〱-〵〸-〼ã-ã‚–ã‚-ã‚Ÿã‚¡-ヺー-ヿㄅ-ㄭㄱ-ㆎㆠ-ㆺㇰ-ㇿã€-䶵一-鿌ꀀ-ê’Œê“-ꓽꔀ-ꘌê˜-ꘟꘪꘫꙀ-ꙮꙿ-êš—êš -ꛯꜗ-ꜟꜢ-ꞈꞋ-ꞎêž-êž“êž -Ɦꟸ-ê ê ƒ-ê …ê ‡-ê Šê Œ-ꠢꡀ-ꡳꢂ-ꢳꣲ-ꣷꣻꤊ-ꤥꤰ-ꥆꥠ-ꥼꦄ-ꦲê§ê¨€-ꨨꩀ-ê©‚ê©„-ê©‹ê© -ꩶꩺꪀ-ꪯꪱꪵꪶꪹ-ꪽꫀꫂꫛ-ê«ê« -ꫪꫲ-ê«´ê¬-ꬆꬉ-ꬎꬑ-ꬖꬠ-ꬦꬨ-ꬮꯀ-ꯢ가-힣ힰ-ퟆퟋ-ퟻ豈-ï©­ï©°-龎ff-stﬓ-ﬗï¬ï¬Ÿ-ﬨשׁ-זּטּ-לּמּנּï­ï­ƒï­„ï­†-ﮱﯓ-ï´½ïµ-ï¶ï¶’-ﷇﷰ-ﷻﹰ-ﹴﹶ-ﻼA-Zï½-zヲ-하-ᅦᅧ-ï¿ï¿’-ï¿—ï¿š-ï¿œ]/: Range out of order in character class
    at new RegExp (<anonymous>)
    at overload.min.js:1208
    at overload.min.js:1018
    at Object.<anonymous> (overload.min.js:1018)
    at Object.19.1YiZ5S (overload.min.js:1212)
    at o (overload.min.js:1)
    at overload.min.js:1
    at Object.<anonymous> (overload.min.js:4)
    at Object.1.1YiZ5S (overload.min.js:11)
    at o (overload.min.js:1)

 エラーを見てみます。SyntaxError: Invalid regular expressionとは、文面からみて正規表現が不正値なのでしょう。その先に続く値をみると、文字化けしているように見えます。おそらく、minifyしたとき、正規表現にセットした値が文字化けしてしまったのだと予想できます。

 コンソールにはエラー箇所が以下のように表示されています。これをクリックすると該当箇所へ移動します。

overload.min.js:1208

 該当コードを見た所、次の二箇所が文字化けしていました。RegExp()の引数が文字化けしています。これを修正すれば解決しそうです。

on={NonAsciiIdentifierStart:new RegExp("[ªµºÀ-ÖØ- ... ]"),
,NonAsciiIdentifierPart:new RegExp("[ªµºÀ-ÖØ-öø ... ]")},

 文字化けする前のコードはoverload.jsファイルから取得できそうです。同じコード箇所を見つけるためのキーワードとして、NonAsciiIdentifierStartが使えそうですね。

2. 非min のコードから対象箇所を参照しコピペする
  1. 非minのコードoverload.jsテキストエディタで開く
  2. 文字列検索NonAsciiIdentifierStartして修正箇所を見つける

 すると修正すべきRegExp()の引数が以下のように表示されていました。(長いので省略)

NonAsciiIdentifierStart: new RegExp('[\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1 ...
NonAsciiIdentifierPart: new RegExp('[\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1

 あとはこれを先述したminのほうのファイルoverload.min.jsの該当箇所に上書きしてやればOKです。

2. 実行する

1. operator-overloading-jsAPIであるoverload()関数が存在するか確認する
vim index.html
<script src="overload.min.js"></script>
<script>
console.log(overload)
</script>

 以下のように表示されたらOKです。

ƒ (e){var t=r.parse("var fn = "+e);if(!t)throw new Error("Invalid code block! Cannot overload. AST Generation Error.");var u=t.body[0].declarations[0].init.params.reduce(function(e,t){return e.push(t.n…
2. 配列に+=したときの処理をオーバーロードしてみる

 ついに本題です。

vim index.html
<script src="overload.min.js"></script>
<script>
Array.prototype.__addAssign = function(v){
    return [...this, ...v]
}
overload(function(){
    let a = [1,2]
    a += [3,4]
    console.log(a)
})();
</script>

 コンソールを見ると次のように表示されました。成功!

[1, 2, 3, 4]

圧縮

 JSCompressを使ってoverload.jsを圧縮すると、minより小さくなりました。

ファイル サイズ
overload.js 401KB
overload.min.js 196KB
compressed.js 147KB

 圧縮アルゴリズムの違いでしょう。例によって正規表現の文字化けバグが発生しました。それを修正すると143KBから147KBになり少し増えたものの最小です。

CodePen

See the Pen Untitled by ytyaru (@ytyaru) on CodePen.

  1. 最高圧縮したライブラリをGitHubにプッシュし、Pagesにデプロイした
  2. そのファイルoperator-overloading-js/min.jsをライブラリとしてCodePenに読み込ませた
  3. 目的の+=演算子オーバーロードを実行した

懸念点

  • 147KBもある
  • overload(function(){...})();で全コードを囲む必要あり
    • コード量が多いと処理も遅くなりそう
  • 結果をDOMに追加できない(エラーになる)

 演算子オーバーロードをするために、これほどのコストをかける価値はあるのか?

 せめてawait overload( a += [3,4] )のように最小スコープでシンプルに書きたかった。

 結果をDOMに追加できないのはJSとして致命的。

 実用性皆無です。

結論

 演算子オーバーロードは一応可能でした。でも非実用的です。EcmaScriptで実装されることを希望します。