Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prefer not in and not instanceof #1

Open
ljharb opened this issue Sep 15, 2023 · 12 comments
Open

Prefer not in and not instanceof #1

ljharb opened this issue Sep 15, 2023 · 12 comments

Comments

@ljharb
Copy link
Member

ljharb commented Sep 15, 2023

Rather than making it more syntax soupy, and combining the ! symbol with a word, "not in" and "not instanceof" read more clearly to me and are much less potentially ambiguous.

This also meshes nicely with pattern matching, in which not would be part of the pattern syntax.

@hax
Copy link
Member

hax commented Sep 17, 2023

!in and !instanceof already valid in TS now. key!in x will be treat as (key!) in x. Though TS can be upgraded to support it, just like how TS deal with x! == value, I feel it's simple to just choose not in.

@ljharb
Copy link
Member Author

ljharb commented Sep 18, 2023

I very much dislike using TS’s choices as a reason to change the language design, even when it supports my preference.

@hax
Copy link
Member

hax commented Sep 19, 2023

I think it's very important to understand how the design affect the popular tools in our ecosystem.

In this specific case, key!in x and key!instanceof x already valid in TS, which means if we adopt !in/!instanceof, it would just has the reversed semantic (due to type erasing just remove ! 🤡 ) in all old TypeScript compilers (and all tools which use such old TS compilers as their dependencies) and very likely have NO WARNING at all. Though linters might save us by enforcing space around ! to make it clear, I feel it's too subtle to rely on whitespaces.

Actually after noticing the ambiguity I finally realized why Kotlin (which adopt !in/!is) choose !! as their null assertion operator. Syntax is always a hard area 😵‍💫

@ljharb
Copy link
Member Author

ljharb commented Oct 5, 2023

I agree that it's a conflict with TS, and that might motivate some to prefer keywords over syntax. I'm just saying that I'm not comfortable with "not JavaScript" forcing the design of JavaScript, even when it supports a change I want :-)

@bathos
Copy link

bathos commented Oct 5, 2023

I also strongly favor not in (and wouldn’t think !in is much of an improvement; key in obj === false, which already works and is absent from the readme, is more readable to me than key !in obj, but key not in obj would be a neat improvement).

@gorosgobe
Copy link
Collaborator

There is indeed a TS incompatibility here. Thanks for pointing this out - this was also raised during the TC39 meeting.

I investigated the issue using Sourcegraph to look at occurrences of !in and !instanceof in open source TypeScript code. Fortunately, it looks like the incompatibility is minor. For !in, there are 4 unique occurrences (filtering out any false positives).
Similarly, for !instanceof, there are 9 unique occurrences.

Out of these 13 unique occurrences, it looks like most if not all usages of !in and !instanceof in TypeScript are bugs. !in and !instanceof in TypeScript would be transpiled into in and instanceof. However, in most of these occurrences, they are used as negated in and negated instanceof.

I've listed some clear bug examples (out of the occurrences) below:

Bug examples
Type Link
!in Link
!in Link
!in Link
!instanceof Link
!instanceof Link
!instanceof Link

I plan to follow up with the TypeScript team to discuss how to treat this incompatibility based on the data above. If possible, I'd like to keep both syntax options open, and choose one or the other based on their own merits.

I also strongly favor not in (and wouldn’t think !in is much of an improvement; key in obj === false, which already works and is absent from the readme, is more readable to me than key !in obj, but key not in obj would be a neat improvement).

key in obj === false is an unnecessary comparison. There is no need to compare a boolean predicate with a boolean literal - you can just use the predicate directly. This is actively discouraged through eslint rules, for example. I also don't really follow why not in would be an improvement, but !in wouldn't be. You could argue that one is more readable than the other, but there is at least one major programming language using either option (Kotlin and Python), and both syntax options address all the issues raised in the README.

@towerofnix
Copy link

towerofnix commented Apr 4, 2024

Didn't see this ratified from a purely in-language context, i.e. without any relation to other programming languages, so here are some observations about existing syntax forms in ECMAScript.

AFAIK there are either zero or few JS keywords which combine symbols and letters to convey meaning — here are the possible exceptions:

  • function* and yield* are both to do with generators.
  • new.target exists.
  • import.meta exists.
  • async x => y exists.

There aren't that many multi-word keywords in JavaScript, but it's nowhere near unheard of:

  • async function
  • class .. extends
  • export const, export var, export function, export class, and of course export default
  • export .. from, arguably export { .. as .. }
  • control flows: do..while is arguably multiple words, even if they're broken around a block! ditto for if..else, switch..case..default, try..except..finally, for..of
  • else if in particular are two words which very commonly occur together as a keyword
  • for await is a another definitive two-word keyword! just much less common
  • arguably break as in label: { ... break label; ... }

Note that I'm not counting unary expressions like typeof or await which just happen to often be called with an identifier (thus another "word") — yes these are a case of two words going together to express something, but here it's an operator and operand, not two words making up one operator/keyword.

There are loads of operations which combine symbols from a particular suite of operations with assignment, getting you a "composite" meaning you can maybe guess the first time you come across &&=, for example:

  • +=, -=, *= etc are "math" plus "assignment"
  • ||=, &&= are "logic" plus "assignment"
  • ^=, &=, >>>= are "bit-math/logic" plus "assignment"

But in general we don't seem to combine symbols in a meaningful manner as much as words.

  • <=, ==, >= are lovely value comparisons, but don't get them mixed up with =>!
  • there isn't a "compact async" form for arrow functions, it's just async foo => bar
  • there aren't "compact generators" at all, if you want it as an expression you have to do (function*(foo) { yield bar })
  • the basic let x = y, const x = y, var x = y forms all use a word and symbol
  • destructuring assignments also usually involve a let/const/var word...
  • ...and always combine assignment (=) with symbols for array/object-ish syntax
  • let x = 3, y = 4, z = x + y arguably combines the word let with symbol , reusing the "comma means more of the same sort of thing please!" syntax found throughout the language

I want to give special attention to the forms else if, async function, export default and for await: these are all combinations of words in ways that are fairly approachable and go together to express something meaningful.


However, I also have to acknowledge that basically all of the examples of combining words happen in statements, not as operators within expressions. We do use words in expressions a lot — await, typeof, instanceof, in! — but there just aren't cases where we stick two words up against each other and say, "look! meaning!". In this regard not in and not instanceof really would be breaking new ground.

Still, I think that's worthwhile ground to break. This is totally an IMO statement, but — IMO 😸 — there just isn't much approachably predictable rhyme or reason to combining symbols with words, or symbols with other symbols, really. See the list above: it's a big mixed bag.

The other inherent advantage to using words for keywords is that they force you, very obviously, to put spaces between the operator and operand. Here's some real shenanigans:

x!instanceof Y  /* The horror! Spacing on the right, none on the left! */

I don't know for sure how ECMAScript parsing works so I'm not sure if this is legal or not, but it certainly looks like it could be... and it just isn't nice. No one wants to figure out what that means. Sure it's on the developer for styling to their own preferences, but x not instanceof Y avoids the opportunity altogether.

(Also a quick case on conciseness: not is a small word. not instanceof is a big operator, but instanceof is already a big operator. not in isn't a very big operator, and neither is in. QED ^^)

@yume-chan
Copy link

The (currently also in stage 1) pattern matching proposal has a not pattern (https://tc39.es/proposal-pattern-matching/#prod-CombinedMatchPattern).

It will allow a is not instanceof T and a is not T (currently with a different semantic, but shouldn't matter in most cases). IMO it reads much better than a !instanceof T. is not is also shorter than !instanceof

@FrameMuse
Copy link

FrameMuse commented Aug 11, 2024

Correct me if I'm wrong here

Rather than making it more syntax soupy, and combining the ! symbol with a word, "not in" and "not instanceof" read more clearly to me and are much less potentially ambiguous.

So you're saying that this is ambiguous for you

try {...} catch (error) {
	if (error !instanceof TypeError) {...}
	throw error
}

But this is completely ok, right?

async function init() {
  if (!navigator.gpu) {
    throw Error("WebGPU not supported.");
  }

  const adapter = await navigator.gpu.requestAdapter();
  if (!adapter) {
    throw Error("Couldn't request WebGPU adapter.");
  }

  const device = await adapter.requestDevice();

  //...
}

Understand me right here, the argument would be valid if we would live in the world where JS has not instead of ! symbol. But this is fair mention that we all are already used to ! symbol even if we would have not wanted to.

You may say that those are different from specification, but from human vision are not.

..., combining the ! symbol with a word ...

Both cases use ! against a word.


This also meshes nicely with pattern matching, in which not would be part of the pattern syntax.

This is unarguable though

@towerofnix
Copy link

towerofnix commented Aug 11, 2024

..., combining the ! symbol with a word ...

Both cases use ! against a word.

I assume by "both cases" you mean the two examples you gave.

It's true that we use ! in the language a lot already. Of course we use it all the time inside if statements as you demonstrate, and in that regard we are basically putting "if" and "!" together, and this creates meaning - meaning that is very glanceable, because of how common if (!..some identifier..) is.

I think it's subjective whether you conceptualize ! as "negate the expression that follows" - associating it with the expression ahead - or "check if this value is absent / falsy" - associating it with the if behind. Both readings are useful and probably both are common. Both lend to combining ! with a word, but I would say the first lends more; there, ! is always combined with if, and in the second, it's more like ! is a generally applicable accent (for any following expression or identifier).

Personally (speaking for myself not ljharb), I wouldn't say that if (x !instanceof y) is confusing - nor ambiguous. Its meaning is obvious, in part thanks to the ways I frequently use if (!dog.sleepy) or if (!foo(bar)) in my own code - as many developers do. I don't think there is a big risk for confusion.

Correct me if I'm wrong here...

I believe (correct me if I'm wrong here LOL) that the main point of "potential ambiguity", though, is to do with TypeScript syntax. Keep in mind in JavaScript we always use ! as a unary prefix operator. In TypeScript it is valid to use ! as a unary postfix operator (or symbol), which has a specific meaning that is completely unrelated to boolean operations or negation.

Prefix (in JavaScript and TypeScript): !x, "not x", "opposite of x's boolean value".
Postfix (in TypeScript only): x!, "x, which certainly is neither null nor undefined".

The operator in this proposal (if not changed to not in, not instanceof) is !in, !instanceof. You can't just peel apart the ! and the in/instanceof, so x ! in y is invalid, for example. This means there is no ambiguity in x! instanceof y.

However, there is nothing saying there has to be whitespace between the left-hand expression and the !in or !instanceof. That means something like this is valid: x!instanceof y.

This is trouble, because in TypeScript, that already has a different meaning. After all, x! means something, there! So, TypeScript has to decide - for themselves - what x!instanceof y means. If they decide it means "check if x is not an instance of y", then they match the JavaScript behavior this spec proposes... but they break existing TypeScript code. If they decide it means "check if x! is an instanceof y", then they keep all existing TypeScript code working... but they fail to match the behavior in JavaScript. Meaning you, any ordinary developer, have to know that x!instanceof y means something different in TypeScript than it does in JavaScript.

@hax summarized the conflict well: #1 (comment)
And @gorosgobe analyzed real code to assess real impact: #1 (comment)
These are worth reading if you want to contextualize the ambiguity point further!

@HolgerJeromin
Copy link

! is always combined with if

Just a small note:
const invert = !myBool; is not unusual. So this partial argument is wrong.

@towerofnix
Copy link

@HolgerJeromin I don't think the argument is wrong per se, it's just — not related to the reading I was pointing to. Like I said, "there," it's "always" combined with if. This doesn't mean you - the real developer - literally never use ! as the unary operator it is in other contexts, only that it isn't relevant to that reading. The point of the reading - in contrast to the opposing reading - is that by conceptually combining if and ! to do something if the expression is false, you are basically making a new keyword in your head, "if-not", if (!...).

In practice it is likely that most developers experience ! in a variety of contexts, including - just for example - as one of the characters in another keyword, !==. It's variable how much you personally encounter...

  • foo = !expr as an assignment value
  • foo(bar, !expr, baz) as an operand in a generic context
  • foo && !(expr) as an operand in a boolean context
  • bim !== bam as part of a keyword based on ===, in comparison, and
  • if (!expr) {...} as part of "if-not", a synonym for if (expr); else {...}

Perhaps you also run into ! in other contexts I haven't thought of.

These are all likely to inform how you see !—and as a result, may play into how you feel about !in and !instanceof, compared to not in and not instanceof. However, I pointed out specifically the nuances of if (!expr). (I.e, how you might recognize it at a glance as simply "if-not", and "boil away" the significance of ! as its own semantic token.)

I understand how it reads like I meant that, if you see "if-not" as its own thing, then you literally never use ! in any other contexts. That wasn't my intention, though. I only meant that "if-not" is a fairly intuitive phenomenon that clearly exists, and that it is a pretty good argument that people are able to understand a glanceable meaning when you persistently put ! and another word (such as in or instanceof) in a nearby space.

Of course, you can basically summarize your understanding of ! as "negate the expression that follows" and still use it in a variety of ways, as you point out. Again I can see how my intention might not come through: I didn't mean that this is always what you understand if you read ! like "negate the expression that follows". I was specifically talking in the context of if (!expr). This isn't literally explicit in that paragraph, but it's implied in how I was following the previous one.

I describe two ways you might relate your understanding of ! to the pattern if (!expr). Both ways describe if (!expr) effectively. However, only one of them lends directly to creating an "if-not" keyword. This is what I wanted to point out: even if we see if (!expr) is a frequent code pattern, how directly it lends to "if-not" as a conceptual keyword depends on how you conceptualize ! - in that context. (The way you think about ! is informed by all the places you see it. But my point only depends on which way you lean right then, reading it in if (!expr).)

TL;DR yes you can encounter ! in lots of contexts and they're all relevant to how you personally understand what ! means, but in terms of if (!expr), only one of the two ways I described lends directly to making a special meaning out of a word ("if") and a symbol (!).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants