Operators: ||==, ||===

Sorry, what did you mean by "a more 'complete set' of equality syntax"?

Comparison operators can either be complete or not. For example, if you only had == and did not have !==, there would be logical statements you simply could not express. A set of operators can either be complete (capable of expressing boolean logic) or not.

Javascript has a complete set of boolean operators. It does not have chaining operators.
If chaining operators were added, they should be boolean-complete.

There are 3 basic boolean operators: &&, ||, !
Mapping those to comparisons yields: &&==, ||==, &&!==, ||!==
If you omit any one of those 4, it is impossible to write boolean logic statements.

If, in addition to boolean logic, you want numeric logic,
there are 3 basic numerical comparison operations: >, <, ==
Mapping those to the boolean comparisons yields: &&>, &&<, ||>, ||<
(plus &&==, ||==, which coincide with boolean operators)
If you omit any of those 4 numeric operators, it is impossible to write numeric logic statements.

The set can either be complete, or incomplete, but I don't see how it can be "less" or "more" complete.

I guess I'm not being clear, my apologies.

You've suggested four operators. Do you have any examples of prior art in other languages for these operators?

Ah, sorry. I understand now.
Unfortunately, I don't think you can google simple symbols like this, and none of the languages I know employ them. (To be fair, besides JS I only know C, x86 asm / machine code, WASM, Python, and Java. Well, other things like SQL and HTML / CSS etc. that aren't relevant here.) Again, chaining operators are rare (and very ambiguous / contextual in Python...), so these might not exist elsewhere.

After researching, I am confident these operators do not exist in other languages.
Strange, when they're so intuitive. Commenters here grasped their use at a glance.

In other languages, chaining comparisons always inspire headaches because they're ambiguous and contextual.

Also, it's not right to call these "chaining" comparisons. They're something like... "multiple" comparisons, maybe?

Other languages do solve the issue, but in other ways.

Let's compare with Python.

Instead of these:

a &&=== b &&=== c
a &&!== b &&!== c
a &&< b &&< c
a &&> b &&> c

you do these:

a == b == c
a != b != c
a < b < c
a > b > c

And instead of these:

a ||=== b ||=== c
a ||!== b ||!== c

you do these:

a in [b, c]
a not in [b, c]

Finally, instead of these:

a ||> b ||> c
a ||>= b ||>= c
a ||< b ||< c
a ||<= b ||<= c

You do these:

a > max(b, c)
a >= max(b, c)
a < min(b, c)
a <= min(b, c)

We can't adopt the a === b === c pattern in JavaScript, as that breaks backwards compatibility, so it's impossible to achieve the same level of elegance in this regard. But, this does show that Python is still solving the same problem, they just use a set of different techniques to do so.

2 Likes

Those are fundamentally different, and I never proposed changing the meaning of === or any other operator. These are exclusively new operators, and every one is a syntax error in existing interpreters.

As for Python, the first isn't related:

Python's a < b < c is equivalent to (a < b) && (b < c)
Proposed here a &&< b &&< c is equivalent to (a < b) && (a < c)

But the other two are just bad designs, right?:

a in [b, c] cannot express a ||=== b ||!== c.
max( b, c ) cannot express x ||< rx1 ||> rx2.

In my humble opinion, three techniques, none of which can achieve what these operators do, is not "solving the same problem", but I only dabble in Python for programming a Raspberry PI, honestly. I'm not competent to critique its design, and I'm not interested in improving it.

I want a shorthand to run multiple logic checks against one variable. There's only one syntactically reasonable way to achieve that, which I proposed here.

For example, how would you express a rectangle-bounds check using the python techniques you described here?

if( x < rect.left || x > rect.right || y < rect.top || y > rect.bottom )
    //out of bounds

//multiple comparison operators:
if( x ||< rect.left ||> rect.right  || y ||< rect.top ||> rect.bottom )
   //out of bounds

(This example doesn't show off a lot of saved space or anything, obviously, but it's just an example of a very common kind of multiple comparisons against one variable. These operators handle it great, because they're straight-forward boolean logic shorthand. A mathematically complete set of use cases, instead of a niche feature.)

1 Like

Whoops, you're right, I missed up there. The equivalent version would actually be:

// before
a &&=== b &&=== c
a &&!== b &&!== c
a &&< b &&< c
a &&> b &&> c

// after
a == b == c // this was still correct
a != b != c // this was still correct
a < min(b, c)
a > max(b, c)

And these were messed up as well, I had the "min"s and "max"es flipped around.

// before
a ||> b ||> c
a ||>= b ||>= c
a ||< b ||< c
a ||<= b ||<= c

// after
a > min(b, c)
a >= min(b, c)
a < max(b, c)
a <= max(b, c)

Yes, I'm just exploring other ways in which some of this problem space can be solved. I'm not necessarily advocating Python's way either, I'm just bringing it up, because it is prior art that does solve some of the issues.

I did forget to address the combination of multiple operators (like x ||< rx1 ||> rx2), so thanks for bringing that up. You're absolutely right that the system you're proposing is more powerful in this regard, and that Python doesn't have an equivalent way to do some of these things (though, in this very specific case, it could be written in Python as rx2 < x < rx1, but other examples that you brought up don't have a Python translation).

I actually prefer the Python solution to this. It looks like this:

// Original
if( x < rect.left || x > rect.right || y < rect.top || y > rect.bottom )
    //out of bounds

// With your proposal
if( x ||< rect.left ||> rect.right  || y ||< rect.top ||> rect.bottom )
   //out of bounds

// Python
in_bounds = rect.left <= x <= rect.right and rect.top <= y <= rect.bottom
if not in_bounds:
   //out of bounds

(edit: Fixed minor bug where I used or instead of and)

Though, again, Python has a solution for this specific problem, but it doesn't have a solution for all the different ways to combine operators, so what you're providing does have some advantages over Python.

Oh, of course. As examples of prior art, these mean the Python spec designers were thinking about similar questions. Definitely my weak point in this discussion.

Here's my take on why you had to rephrase the logic, and why it matters:

For rectangles (this is relevant, promise), you never do an in-bounds check. In-bounding requires evaluating all 4 comparisons. You always do an out-of-bounds check, because it short-circuits at the first evaluation that proves true. You save thousands of ops across every draw frame. (And you always start by checking x, because there are more horizontal pixels than vertical; but that's neither relevant nor all that true these days. :-) )

Honestly in this rectangle example, we don't need to optimize JavaScript to save ops-per-frame like that (just use WASM or GLSL) but the point is: Using that syntax forced you into a non-optimal solution. Sometimes, there isn't even a non-optimal solution; that syntax is just unusable for certain cases.

These operators can handle the out-of-bounds check easily for one very simple reason: they're just a complete boolean logic set. They can handle every logic case, mathematically guaranteed.

Oh, and of course, these operators can also do the in-bounds check, although not as prettily as Python I think. :-) (The way Python used those operators reminds me of how we wrote bound notations in math class.)

if( x &&> rect.left &&< rect.right && y &&> rect.top &&< rect.bottom )

Not claiming that the Python language designers were specifically thinking of this problem when they designed the language :) - just saying that some of their syntax choices do partially solve some of the problem space that we're exploring here. I'm not even trying to advocate that we copy them in any way - it's mostly just "hey this relates to the problem space, so I want to share it".

Yep, this is all true. So, yes, this does expose other ways in which what you're proposing is more powerful than Python's syntax.

Hey @0X-JonMichaelGalindo

Here is something similar I proposed a while ago. The exact syntax isn't final but imho, the general idea is more powerful than yours as it can support more operators than just == and ===.

I know this is a very long thread, but you missed that part of my proposal. I'll copy it below.

I have a partial proposal for logic operators and an extended one for numeric operators. But they are guaranteed mathematically complete. Yours might be as well, if I understand correctly.

There are 3 basic boolean operators: &&, ||, !
Mapping those to comparisons yields: &&==, ||==, &&!==, ||!==
If you omit any one of those 4, it is impossible to write boolean logic statements.

If, in addition to boolean logic, you want numeric logic,
there are 3 basic numerical comparison operations: >, <, ==
Mapping those to the boolean comparisons yields: &&>, &&<, ||>, ||<
(plus &&==, ||==, which coincide with boolean operators)
If you omit any of those 4 numeric operators, it is impossible to write numeric logic statements.

The set can either be complete, or incomplete, but I don't see how it can be "less" or "more" complete.

I'm not liking this because of its context-dependency. If I write:

let x = "foo";
let y = ( x ||=== "bar");
let z = ( y ||=== "foo");

I think y and z are both false, whereas if I had

let z = ( x ||=== "bar" ||=== "foo" );

z would be true. This breaks my model of what operators do, which expects context-freedom.

I suspect the same goals could be achieved with sets and ranges. something like

if (txt in {{foo, bar}}) doStuff();
if (x in [[left, right]]) hit();

Well, I'm moderately sure it will never be added to the language, so I'm not too worried if there does turn out to be some problem I overlooked. :-)

But for the issues you raised:

Lone Chaining Operator

x ||=== "bar"; //syntax error

A chaining operator without a chain is technically a meaningless "either" operator in an incomplete "either / or" statement. (What would it do? Would it only test equality? :thinking: ) But strictly, it should be forbidden. It would be confusing.

Context Dependency

This breaks my model of what operators do, which expects context-freedom

I understand that a lone chaining operator would be confusing, but I am not sure I understood your point about context-dependency. Here's my best answer, so please excuse me if it is unrelated:

Yes, the proposed operator's behavior is contextual to the expression.
Conditional operators' evaluations are inherently contextual to their expressions, for example:

let x = true && ... && true || false && true ... && someFunc();

Where ... represents a hidden part of the expression.
Is x true? Does someFunc() ever evaluate? Unknowable.
Conditional operators remain expression-context-dependent in this proposal. (Not 100% sure that's what you meant though.)

Array-Like Alternatives

I suspect the same goals could be achieved with sets and ranges. something like

I've answered this a few times, but I know the thread's too long to read. Here's the recap.
This is impossible with set- and range-based approaches:

if( x &&>= rect.left &&<= rect.right ) {
    ...
}

In principle, all expressions like this one are impossible with sets, ranges, arrays, and functions. (The array functions for(), reduce(), some(), and every() cannot do it. No functional programming approach can.)

__

Honestly, I wrote this proposal a long time ago, and I feel less than familiar with the reasons behind it these days.

I do remember having very clear reasons for a great many decisions. They might come back to me with questions like yours. Most of those answers are probably covered in the pages here as well.

I think these operators would be useful, and I think people would use them; but of course, not everyone would. I don't use classes, for example, and I know many coders use classes for everything.

In any case, thank you for taking the time to read and react. :-)

Here's a thought. Although I'm not that fond of the idea behind this operator, it could still be made to work in array-like scenarios:

if ( x &&>= [rect.left]  &&<= [rect.right] ) {
   ...
}

That would be the notation. It's just a matter of proper grouping. However in this scenario, I'd expect a syntax error because the operator expects to reuse the lhs operand on multiple elements found in the rhs array operand. Nevermind the fact that this syntax (whether this version or yours) would be a complete waste in this scenario since:

if ((x >= rect.left) && (x <= rect.right)) {
   ...
}

...isn't significantly different in size and is much clearer to read.

The main advantage of such a SIMD type instruction is it's ability to apply the same operation to all elements of a data set. Changing the operator midway in the statement doesn't give much benefit over doing it manually while also making it harder to understand what's being done. If this is to be used at all, it's best to keep with the SIMD metaphor. That lends it to actual optimization advantages as well as improved clarity of intent.

I'm surprised no one here mentioned C#'s pattern-matching operator is, which exactly fills OP's needs and goes a fair bit beyond by allowing operations other than just equality.

From the C# reference on the pattern operator:

static bool IsLetter(char c) =>
  c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');

C# uses the same logical operators as ECMAScript (&&, ||, and !) so that and and or you see there aren't the classic boolean operations, they're context-specific keywords specifically for pattern matching.

is, on the other hand, is C#'s version of instanceof, so they went the "overload an existing syntax operator" route - they broadened the semantics of is from "value is of type X" to "value matches pattern X", and they defined "bare type name" as a valid pattern meaning "can be assigned to this type".

There's also a super convenient shorthand that comes of this, where you can type-test and cast/assign at the same time:

// JS code, naΓ―ve implementation that can lead to unhandled exceptions
function setTristateCheckbox(elementId, state) {
    document.getElementById(elementId).checked = !!state;
    document.getElementById(elementId).indeterminate = state === null;
}
// same function, JS code with input checking
function setTristateCheckbox(elementId, state) {
    const elem = document.getElementById(elementId)
    if (elem instanceof HTMLInputElement && element.type === "checkbox") {
        elem.checked = !!state;
        elem.indeterminate = state === null;
    }
}
// same function, transliterated C# syntax for pattern matching
function setTristateCheckbox(elementId, state) {
    if (document.getElementById(elementId)
            is HTMLInputElement cbox and {type: "checkbox"}) {
        // "is" pattern matching rules also power C#'s switch expression:
        [cbox.checked, cbox.indeterminate] = (state switch {
            null => [false, true],
            _ => [!!state, false],
        });
    }
}

Obviously if ECMAScript were to adopt something like this it'd need some modification, especially since unlike with C#, it is not so easy to tell if a value is a type (wants "instanceof" handling) or a pattern object like the {type: "checkbox"} above (wants property matching) or a literal like the implicit if (state is null) from the first branch of the switch expression (wants reference equality). But I think it would be not at all unreasonable to have some sort of ECMAScript pattern-matching expression, which might let you write something like this:

if (someBigCalculation()
    is const result // "const $VAR" always succeeds and also binds a new variable
    and typeof === "number" // special `typeof===` syntax for patterns, perhaps
    and isFinite // valid standalone expression is taken as a test predicate?
    and % 2 === 0
    and >= 0) 
{
    console.log("Got non-negative even number:", result);
} else {
    console.log("Not the right result:", resule);
}
// unlike the declarations in a for statement, variables bound in a pattern
// stay in scope for the rest of the containing block - very important!
return result;

// this is why that's important:
if (map.get(key) is let val and not instanceof UsefulType) {
    val = new UsefulType();
    map.set(key, val);
}
doStuffWith(val);

I'd suggest the following as a starting point for "what patterns could be reasonably parsed and supported in ECMAScript", if someone wanted to run with this and turn it into a proposal:

  • valid expression: must resolve to a callable, succeeds if calling it with the tested value is truthy
  • begins with a binary boolean-result operator: expression must parse successfully if you put the tested value in front of it, succeeds if the resulting expression resolves to a truthy value
  • const, let, or var followed by an identifier: always succeeds, binds the value to the name with standard semantics (const and let are in scope from the byte position where they appear to the end of the block in which the expression appears)
  • typeof followed by === or !== and then an expression (could even be restricted to "and then a string literal")
  • the special keywords truthy, falsy, or nullish, meaning value is truthy becomes an alternative syntax to !!value

You could use a different set of patterns, but these can all be distinguished at parse-time, which is important for implementations.

An is operator is included in the pattern matching proposal for ECMAScript.

1 Like

oh neat, a proposal from Kat! haven't heard that name in an age.

That said, if that were accepted as it is now, it wouldn't be a fit for OP's use case - it's too verbose, since (as the proposal mentions) its primary use case is structural matching.

In a way, structural matching and operator matching are complements of each other; you can express either using the other, but they "natively" support a mutually exclusive set of tests. Pattern matching operates best on truthy, object-like values, while operator matching operates best on primitives.

I think my chief complaint about the is operator expressed in that proposal is that it requires its RHS to be a valid expression - that is to say, it's a binary operator. That adds syntax burden to what should be an extremely syntax-light operation (comparing primitives, like in OP's use case), but more importantly, it also adds engine burden - if the RHS to is is a function call or a bare identifier, then the behavior of the operator is almost certainly unpredictable at compile-time, which eliminates a lot of potential optimization routes.

In comparison, the is I suggested above, along with C#'s version of the operator, is a unary postfix operator which introduces a new and syntactically distinct parser context until it has consumed its argument (not technically an RHS as far as operators are concerned). That means that the compiler can look at that IsLetter function defined up there and just instantly turn it into a [a-zA-Z] character class, without having to wait until it gets called.

I'd honestly be pretty sad if that proposal made it into ECMAScript as it is because it would close the door on some programmer-friendly syntaxes like the above, but given that it's stage 1 I'm not concerned :sweat_smile: