演算子オーバーロードする
ライブラリを使用して実現できましたが、実用性皆無です。
成果物
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 | これならイケる? |
他、valueOf
やdefineProperty
を使って演算子オーバーロード風に書けないか挑戦した記事もありますが、実装できません。使用された演算子が取得できないからです。それが可能な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は嫌だ。
- Node.js
- npm
- bower
- npm
Bower
- インストールする
- Node.js
- Bower
- operator-overloading-js
- 参照する
- 実行する
インストールする
まず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>
実行する
早速ライブラリを使おう! と思ったらバグりました。修正してから実行します。
- バグ修正
- min が文字化けして使えない
- 非min のコードから対象箇所を参照しコピペする
- 実行する
- operator-overloading-jsのAPIである
overload()
関数が存在するか確認する - 配列に
+=
したときの処理をオーバーロードしてみる
- operator-overloading-jsのAPIである
バグ修正
残念ながら最初からバグに遭遇しました。
min が文字化けして使えない
operator-overloading-jsのAPIである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 のコードから対象箇所を参照しコピペする
- 非minのコード
overload.js
をテキストエディタで開く - 文字列検索
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-jsのAPIである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.
- 最高圧縮したライブラリをGitHubにプッシュし、Pagesにデプロイした
- そのファイルoperator-overloading-js/min.jsをライブラリとしてCodePenに読み込ませた
- 目的の
+=
演算子オーバーロードを実行した
懸念点
- 147KBもある
overload(function(){...})();
で全コードを囲む必要あり- コード量が多いと処理も遅くなりそう
- 結果をDOMに追加できない(エラーになる)
演算子オーバーロードをするために、これほどのコストをかける価値はあるのか?
せめてawait overload( a += [3,4] )
のように最小スコープでシンプルに書きたかった。
結果をDOMに追加できないのはJSとして致命的。
実用性皆無です。
結論
演算子オーバーロードは一応可能でした。でも非実用的です。EcmaScriptで実装されることを希望します。