演算子の多重定義

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"名は単項-に対応しています。"++"の多重定義は、増分前の++xと増分後のx++のどちらにも機能し、"--"も同様です。"=="の多重定義は、等価x == yテストと非等価x != yテストの両方に使用されます。同様に、"<"の多重定義は、すべての比較演算子(x < yx <= yx > yx >= y)で、引数の入替えまたは結果の否定(あるいはその両方)によって使用されます。

演算子名に割り当てる値は、二項演算子の場合は2つの引数の関数、単項演算子の場合は1つの引数の関数である必要があります。

table引数には、openプロパティを指定することもできます。その場合、そのプロパティの値は演算子名の配列である必要があります。これらは、将来のクラスがこの型に多重定義できる演算子です(たとえば、Vector型で、後でMatrix型が操作Vector * MatrixおよびMatrix * Vectorを多重定義できるように、"*"がオープンしていることを宣言します)。openプロパティがない場合、すべての演算子は、将来の他の型の多重定義のためにオープンしているとみなされます。

最初の引数tableの後に、オプションの引数extraTablesが続きます。これらも、それぞれがオブジェクトである必要があります。追加の各表には、leftプロパティまたはrightプロパティのいずれかを含める必要があります(両方を含めることはできません)。そのプロパティの値は、次のいずれかのJavaScriptコンストラクタである必要があります:

追加の表の他のプロパティも、最初のtable引数と同様に演算子の多重定義である必要があります(キーが演算子名、値が演算子を実装する関数)。

これらの追加の表は、オペランド型の1つが、定義されている型以外の型であるときの演算子の動作を定義します。追加の表にleftプロパティがある場合、左側のオペランドの型がleftプロパティで指定された型で、右側のオペランドの型に演算子が定義されていると、その表の演算子定義が適用されます。rightプロパティについても同様で、追加の表にrightプロパティがある場合、右側のオペランドに型が指定されており、左側のオペランドの型に演算子が定義されていると、表の演算子定義が適用されます。

カスタム型とJavaScript数値型NumberおよびBigIntの間には、任意の二項演算子を自由に多重定義できます。ただし、カスタム型とString型の間で多重定義できる演算子は、"==""<"のみです。

Operators関数により返されるコンストラクタは、通常は独自のクラス内で拡張します。そのクラスのインスタンスは、多重定義された演算子の定義に従います。多重定義された演算子を持つオブジェクトで演算子を使用すると、常に次のことが発生します:

1) 多重定義された演算子を持たないすべてのオペランドはプリミティブに強制変換されます。2) このオペランドのペアリングに適用可能な多重定義がある場合は、それがコールされます。それ以外の場合は、TypeErrorがスローされます。

多重定義された演算子を持つオブジェクトは、演算子の適用時にプリミティブに強制変換されず、未定義の演算子をそれに適用するとTypeErrorが発生することがあります。これには、2つの例外があります:

1) +演算子を使用していて、いずれかの引数が文字列(またはToPrimitiveを介して文字列に強制変換する多重定義された演算子のないオブジェクト)の場合、結果は2つのオペランドのToString値を連結したものになります。2) ==演算子を使用していて、適用可能な多重定義が見つからない場合、2つのオペランドは異なるものとみなされます(x == yfalseを返し、x != ytrueを返します)。

提案との違い

(仕様およびプロトタイプ実装で定義されている)提案と、GraalVM JavaScriptでの実装には、いくつかの違いがあります: