Just another suggestion (with Symbol) #66
Description
I think of a possible implementation like Symbol.toPrimitive
, similar to #65 but slightly different...
Click to collapse
/**
* possible new JSDoc for classes: <at>operator [RESULT_TYPE] [OTHER_TYPE] OPERATOR [DESCRIPTION]
* ~ RESULT_TYPE takes priority over OTHER_TYPE if only one of the two is specified; both are optional
*
* @operator {boolean} == → returns a boolean for `self==other` (other can be anything)
* @operator {number} +U → returns a number for `+self`
* @operator {number} {number} * → returns a number for `self*other` when other is a number
* @operator {number} {number} / → returns a number for `self/other` when other is a number
*/
class Example(){
/** @type {unknown} example property */test;
/**
* overrides all operator behaviour with this object (called just before `Symbol.toPrimitive`/type coercion)
* @param {string} operator - JS operator
* @param {unknown} other - other object, `undefined` is used for unary operators
*/
[Symbol.oOverload](operator, other){
switch(operator){
// numerics
case "+U": return +this.test; // use `Symbol.toPrimitive` with hint number if not implemented here
case "-U": return -this.test; // use `Symbol.invert` if not implemented here
case "+": return this.test+other; // use `Symbol.oAdd` if not implemented here
case "-": return this.test-other; // use `Symbol.oSub` if not implemented here
case "*": return this.test*other; // use `Symbol.oMul` if not implemented here
case "/": return this.test/other; // use `Symbol.oDiv` if not implemented here
case "**": return this.test**other; // use `Symbol.oPow` if not implemented here
case "%": return this.test%other; // use `Symbol.oMod` if not implemented here
case "++": return ++this.test; // use `Symbol.increment` if not implemented here
case "--": return --this.test; // use `Symbol.decrement` if not implemented here
// bitwise
case "~": return ~this.test; // use `Symbol.binNOT` if not implemented here
case "&": return this.test&other; // use `Symbol.binAND` if not implemented here
case "|": return this.test|other; // use `Symbol.binOR` if not implemented here
case "^": return this.test^other; // use `Symbol.binXOR` if not implemented here
case "<<": return this.test<<other; // use `Symbol.binLShift` if not implemented here
case ">>": return this.test>>other; // use `Symbol.binRShift` if not implemented here
case ">>>": return this.test>>>other; // use `Symbol.binURShift` if not implemented here
// comparison
case "==": return this.test==other; // use `Symbol.comparison` if not implemented here
case "<": return this.test<other; // use `Symbol.comparison` if not implemented here
case ">": return this.test>other; // use `Symbol.comparison` if not implemented here
case "<=": return this.test<=other; // use boolean inverse of `>` if not implemented here
case ">=": return this.test>=other; // use boolean inverse of `<` if not implemented here
// assign
case "=": return this.test=other; // use `Symbol.assign` if not implemented here
// assign numerics
case "+=": return this.test+=other; // use `+` then `=` if not implemented here
case "-=": return this.test-=other; // use `-` then `=` if not implemented here
case "*=": return this.test*=other; // use `*` then `=` if not implemented here
case "/=": return this.test/=other; // use `/` then `=` if not implemented here
case "**=": return this.test**=other; // use `**` then `=` if not implemented here
case "%=": return this.test%=other; // use `%` then `=` if not implemented here
// assign bitwise
case "&=": return this.test&=other; // use `&` then `=` if not implemented here
case "|=": return this.test|=other; // use `|` then `=` if not implemented here
case "^=": return this.test^=other; // use `^` then `=` if not implemented here
case "<<=": return this.test<<=other; // use `<<` then `=` if not implemented here
case ">>=": return this.test>>=other; // use `>>` then `=` if not implemented here
case ">>>=": return this.test>>>=other; // use `>>>` then `=` if not implemented here
}
return Symbol.oOverloadAbort; // override not implemented ~ continue with `Symbol.toPrimitive`/type coercion
// ! without explicit return, `undefined` is used as if it were intentionally returned
}
// more specific overrides can be used if only some functionality needs to be custom-tailored
// --- bitwise ---
/**
* used if `~` operator is not overridden by `Symbol.oOverload`
* @returns {unknown}
*/
[Symbol.binNOT](){ return ~this.test; }
/**
* used if `&` operator is not overridden by `Symbol.oOverload`
* @param {unknown} other
* @returns {unknown}
*/
[Symbol.binAND](other){ return this.test&other; }
/**
* used if `|` operator is not overridden by `Symbol.oOverload`
* @param {unknown} other
* @returns {unknown}
*/
[Symbol.binOR](other){ return this.test|other; }
/**
* used if `^` operator is not overridden by `Symbol.oOverload`
* @param {unknown} other
* @returns {unknown}
*/
[Symbol.binXOR](other){ return this.test^other; }
/**
* used if `<<` operator is not overridden by `Symbol.oOverload`
* @param {unknown} other
* @returns {unknown}
*/
[Symbol.binLShift](other){ return this.test<<other; }
/**
* used if `>>` operator is not overridden by `Symbol.oOverload`
* @param {unknown} other
* @returns {unknown}
*/
[Symbol.binRShift](other){ return this.test>>other; }
/**
* used if `>>>` operator is not overridden by `Symbol.oOverload`
* @param {unknown} other
* @returns {unknown}
*/
[Symbol.binURShift](other){ return this.test>>>other; }
// --- numeric ---
/**
* used if (unary) `-` operator is not overridden by `Symbol.oOverload`
* @returns {unknown}
*/
[Symbol.invert](){ return -this.test; }
/**
* used if `+` operator is not overridden by `Symbol.oOverload`
* @param {unknown} other
* @returns {unknown}
*/
[Symbol.oAdd](other){ return this.test+other; }
/**
* used if `-` operator is not overridden by `Symbol.oOverload`
* @param {unknown} other
* @returns {unknown}
*/
[Symbol.oSub](other){ return this.test-other; }
/**
* used if `*` operator is not overridden by `Symbol.oOverload`
* @param {unknown} other
* @returns {unknown}
*/
[Symbol.oMul](other){ return this.test*other; }
/**
* used if `/` operator is not overridden by `Symbol.oOverload`
* @param {unknown} other
* @returns {unknown}
*/
[Symbol.oDiv](other){ return this.test/other; }
/**
* used if `**` operator is not overridden by `Symbol.oOverload`
* @param {unknown} other
* @returns {unknown}
*/
[Symbol.oPow](other){ return this.test**other; }
/**
* used if `%` operator is not overridden by `Symbol.oOverload`
* @param {unknown} other
* @returns {unknown}
*/
[Symbol.oMod](other){ return this.test%other; }
/**
* used if `++` operator is not overridden by `Symbol.oOverload`
* @returns {unknown}
*/
[Symbol.increment](){ return ++this.test; }
/**
* used if `--` operator is not overridden by `Symbol.oOverload`
* @returns {unknown}
*/
[Symbol.decrement](){ return --this.test; }
// --- other ---
/**
* used if `=` operator is not overridden by `Symbol.oOverload`
* @param {unknown} value
* @returns {unknown}
*/
[Symbol.assign](value){ return this.test = value; }
/**
* used for internal comparisons like {@linkcode Array.sort} and fallback if comparison operators are not overridden in `Symbol.oOverload`
* @param {unknown} other
* @returns {-1|0|1} if {@linkcode other} is larger: `-1`, is smaller: `1`, is equal: `0`
*/
[Symbol.comparison](other){
if(this.test===other)return 0;
if(this.test<other)return -1;
return 1;
}
/**
* used for coercion to boolean (before `Symbol.toPrimitive`)
* @returns {boolean} if this obj is truthy or not
*/
[Symbol.toBoolean](){ return Boolean(this.test); }
/**
* used for numeric property access/assign (after testing if that property exists on this object and failing)
* @param {number|bigint} index - indexed property
* @param {boolean} assign - `true` when assigning to that property
* @param {unknown} other - value for assignment
* @returns {unknown}
*/
[Symbol.indexAccess](index, assign, other){
if(assign)return this.test[index] = other;
return this.test[index];
}
}
The unary +
and -
operators are specified as +U
and -U
, so no other parameter is needed to distinguish them from +
and -
in Symbol.oOverload
and JSDoc.
So, for any A # B
where A
is an object and #
is an operator, do:
- if
A
is an Object- if
A[Symbol.oOverload]
is a function that exists inA
- set
tmp
toA[Symbol.oOverload]("#", B)
- if
tmp
is notSymbol.oOverloadAbort
-
use
tmp
-
- set
- if a more specific method like
Symbol.comparison
exists inA
-
use the result of that specific method
-
- if changing operator is possible (like
+=
to+
and=
)-
go back to step 1.1 with new operator/s
-
- if
- continue with
Symbol.toPrimitve
/type coercion
And, for any # A
where # is an operato,r do:
- if
A
is an Object- if
A[Symbol.oOverload]
is a function that exists inA
- set
tmp
toA[Symbol.oOverload]("#", undefined)
- if
tmp
is notSymbol.oOverloadAbort
-
use
tmp
-
- set
- if a more specific method like
Symbol.increment
exists inA
-
use the result of that specific method
-
- if changing operator is possible (like
++
to+=
with1
)-
go back to step 1.1 with new operator/s
-
- if
- continue with
Symbol.toPrimitve
/type coercion
Also, A++
is the same as ++A
but evaluated after the expression A
is in (if any), same for A--
.
So, for example, A++*B
=> A*B,++A
(exec in that order, but this expression itself returns the result of A*B
not ++A
).
IMO, instead of making boolean operators overrideable, a Symbol.toBoolean
method that evaluates the (current) truthiness of the object would be better.
And the boolean assignments &&=
and ||=
would use Symbol.toBoolean
and assign (via override from Symbol.oOverload
or Symbol.assign
) accordingly.
Also, the Symbol.comparison
can be used internally without needing to give Array.sort
a compare function.
I agree that strict equals and the nullish coalescing operators/assignment should not be overrideable.
Along with the others listed like ,
and ()
.
The main benefit is that it does not break older code, can be easily type-checked (especially when using JSDoc), and should not be too hard to implement.
It also does not add more syntax to the language, except the suggested JSDoc tag.
PS: I don't have deep knowledge of JS engines, so I don't truly know how hard it would be to add support for this :P
PPS: Of course, the symbol names can be changed; after all, naming things is hard ^^