演算子の多重定義
GraalVM JavaScriptでは、ECMAScriptの演算子多重定義提案の初期実装を提供しています。これにより、JavaScriptクラスに対するJavaScriptの演算子の動作を多重定義できます。
この機能を試す場合は、まずそれを有効にする必要があります。提案とその実装はどちらも初期段階にあるため、次の試験段階オプションを設定する必要があります。
js --experimental-options --js.operator-overloading
このオプションを設定すると、グローバル・ネームスペースに新しい組込み関数Operators
が表示されます。この関数をコールして、引数としてJavaScriptオブジェクトを渡すことができます。このオブジェクトには、多重定義する各演算子のプロパティが必要です。キーは演算子の名前で、値はそれを実装する関数です。Operators
関数の戻り値はコンストラクタで、型を定義するときにこれをサブクラス化できます。このコンストラクタをサブクラス化することで、Operators
関数の引数で定義した、多重定義された演算子の動作をすべて継承するオブジェクトが属するクラスを取得します。
基本的な例
ベクトルを使用する元の提案の例を見てみましょう:
const VectorOps = Operators({
"+"(a, b) {
return new Vector(a.contents.map((elt, i) => elt + b.contents[i]));
},
"=="(a, b) {
return a.contents.length === b.contents.length &&
a.contents.every((elt, i) => elt == b.contents[i]);
},
});
class Vector extends VectorOps {
contents;
constructor(contents) {
super();
this.contents = contents;
}
}
ここでは、2つの演算子(+
および==
)の多重定義を定義します。多重定義された演算子の表を使用してOperators
関数をコールすると、VectorOps
クラスが生成されます。次に、Vector
クラスをVectorOps
のサブクラスとして定義します。
Vector
のインスタンスを作成すると、それらが多重定義された演算子の定義に従っていることがわかります:
> new Vector([1, 2, 3]) + new Vector([4, 5, 6]) == new Vector([5, 7, 9])
true
複合型の例
また、異なる型の値間で演算子を多重定義して、たとえばベクトルを数値で乗算することもできます。
const VectorOps = Operators({
"+"(a, b) {
return new Vector(a.contents.map((elt, i) => elt + b.contents[i]));
},
"=="(a, b) {
return a.contents.length === b.contents.length &&
a.contents.every((elt, i) => elt == b.contents[i]);
},
}, {
left: Number,
"*"(a, b) {
return new Vector(b.contents.map(elt => elt * a));
}
});
class Vector extends VectorOps {
contents;
constructor(contents) {
super();
this.contents = contents;
}
}
複合型演算子を定義するには、追加のオブジェクトをOperators
関数に渡す必要があります。これらの追加の表には、動作を多重定義する他の型の演算子が左側にある場合はleft
プロパティ、右側にある場合はright
プロパティを含める必要があります。この例では、左側にNumber
があり、右側にVector
型がある場合に、*
演算子を多重定義しています。追加の各表には、left
プロパティまたはright
プロパティのいずれかと、その特定のケースに適用される演算子多重定義をいくつでも含めることができます。
これを実際に見てみましょう:
> 2 * new Vector([1, 2, 3]) == new Vector([2, 4, 6])
true
参照
関数Operators(table, extraTables...)
は、多重定義された演算子を持つクラスを返します。ユーザーは、そのクラスを拡張する独自のクラスを定義する必要があります。
table
引数は、多重定義された演算子ごとに1つのプロパティを持つオブジェクトである必要があります。プロパティ・キーは演算子の名前である必要があります。多重定義できる演算子の名前は次のとおりです:
- 二項演算子:
"+"
、"-"
、"*"
、"/"
、"%"
、"**"
、"&"
、"^"
、"|"
、"<<"
、">>"
、">>>"
、"=="
、"<"
- 単項演算子:
"pos"
、"neg"
、"++"
、"--"
、"~"
演算子名"pos"
は単項+
に、演算子名"neg"
名は単項-
に対応しています。"++"
の多重定義は、増分前の++x
と増分後のx++
のどちらにも機能し、"--"
も同様です。"=="
の多重定義は、等価x == y
テストと非等価x != y
テストの両方に使用されます。同様に、"<"
の多重定義は、すべての比較演算子(x < y
、x <= y
、x > y
、x >= y
)で、引数の入替えまたは結果の否定(あるいはその両方)によって使用されます。
演算子名に割り当てる値は、二項演算子の場合は2つの引数の関数、単項演算子の場合は1つの引数の関数である必要があります。
table
引数には、open
プロパティを指定することもできます。その場合、そのプロパティの値は演算子名の配列である必要があります。これらは、将来のクラスがこの型に多重定義できる演算子です(たとえば、Vector
型で、後でMatrix
型が操作Vector * Matrix
およびMatrix * Vector
を多重定義できるように、"*"
がオープンしていることを宣言します)。open
プロパティがない場合、すべての演算子は、将来の他の型の多重定義のためにオープンしているとみなされます。
最初の引数table
の後に、オプションの引数extraTables
が続きます。これらも、それぞれがオブジェクトである必要があります。追加の各表には、left
プロパティまたはright
プロパティのいずれかを含める必要があります(両方を含めることはできません)。そのプロパティの値は、次のいずれかのJavaScriptコンストラクタである必要があります:
Number
BigInt
String
- 多重定義された演算子を持つクラス(つまり、
Operators
によって返されたコンストラクタから拡張されたもの)
追加の表の他のプロパティも、最初のtable
引数と同様に演算子の多重定義である必要があります(キーが演算子名、値が演算子を実装する関数)。
これらの追加の表は、オペランド型の1つが、定義されている型以外の型であるときの演算子の動作を定義します。追加の表にleft
プロパティがある場合、左側のオペランドの型がleft
プロパティで指定された型で、右側のオペランドの型に演算子が定義されていると、その表の演算子定義が適用されます。right
プロパティについても同様で、追加の表にright
プロパティがある場合、右側のオペランドに型が指定されており、左側のオペランドの型に演算子が定義されていると、表の演算子定義が適用されます。
カスタム型とJavaScript数値型Number
およびBigInt
の間には、任意の二項演算子を自由に多重定義できます。ただし、カスタム型とString
型の間で多重定義できる演算子は、"=="
と"<"
のみです。
Operators
関数により返されるコンストラクタは、通常は独自のクラス内で拡張します。そのクラスのインスタンスは、多重定義された演算子の定義に従います。多重定義された演算子を持つオブジェクトで演算子を使用すると、常に次のことが発生します:
1) 多重定義された演算子を持たないすべてのオペランドはプリミティブに強制変換されます。2) このオペランドのペアリングに適用可能な多重定義がある場合は、それがコールされます。それ以外の場合は、TypeError
がスローされます。
多重定義された演算子を持つオブジェクトは、演算子の適用時にプリミティブに強制変換されず、未定義の演算子をそれに適用するとTypeError
が発生することがあります。これには、2つの例外があります:
1) +
演算子を使用していて、いずれかの引数が文字列(またはToPrimitive
を介して文字列に強制変換する多重定義された演算子のないオブジェクト)の場合、結果は2つのオペランドのToString
値を連結したものになります。2) ==
演算子を使用していて、適用可能な多重定義が見つからない場合、2つのオペランドは異なるものとみなされます(x == y
はfalse
を返し、x != y
はtrue
を返します)。
提案との違い
(仕様およびプロトタイプ実装で定義されている)提案と、GraalVM JavaScriptでの実装には、いくつかの違いがあります:
- 多重定義された演算子を使用できるようにするために、
with operators from
構成を使用する必要はありません。クラスの演算子を多重定義すると、with operators from
を使用しなくても、それらの演算子を任意の場所で使用できます。さらに、パーサーはwith operators from
句を有効なJavaScriptとして受け入れなくなります。 - デコレータを使用して、多重定義された演算子を定義することはできません。この提案の実装時点では、GraalVM JavaScriptはデコレータをサポートしていません(まだ最終決定していない提案です)。
- 整数で索引付けされた要素の読取りおよび書込みのために、
"[]"
および"[]="
演算子を多重定義することはできません。この2つの演算子には、より複雑な処理が必要であり、現時点ではサポートされていません。