1. 10
  1. 17

    You wrote that you’d like this to be possible:

    let generatePerson =
      name => {name, id: generateId()}
    

    [In today’s JavaScript] instead you need to do:

    let generatePerson = (name) => {
      return { name, id: generateId() };
    };
    

    To make your code work with today’s JavaScript, you only need to wrap the object literal in parentheses (docs):

    let generatePerson =
      name => ({name, id: generateId()})
    
    1. 3

      No, they actually said that, it doesn’t work but it should because it follows from the syntax afterwards that we can’t do anything else.

      I’m a way it’s a cool idea, but what if you really wanted to open that block, but mistyped something? Now instead of a syntax error your little helper does something unexpected.

      The imports point is partially okay as well, but the tooling is already good enough that I don’t know what’s up there, it just works.

      1. 2

        I’d be somewhat partial to your point if this wasn’t JavaScript, but it is. It’s already a language where mistyping something causes any kind of early hard error is the exception, not the rule. I don’t think this particular case would make the problem worse in any perceptible way.

        1. 1

          Perhaps, but the tooling is working wonders, compared to, say, 10 years ago.

          All I’m saying is that these seem like nice ideas but they seem to me like they’re not thought entirely through.

    2. 14

      I do not like this article, it seems very blasé about the cost of breaking syntax, uninterested in why decisions about these things (like the syntax of async/await) were made, and it seems to lack a practical understanding of how language features have to mechanically work in dynamic languages.

      Kill function scope and fix binding keywords

      If this matters so much, a lint tool to remove var is not hard, and doesn’t break any existing code, or require your site to ensure it never combines the wrong libraries together in single responses. A mode switch to remove var seems absurdly expensive.

      Fix import keyword ordering

      This makes it seem like the keyword ordering is an unintentional mistake, which it isn’t. It’s the result of unending discussions, and the end result was that the current ordering was the most sensible, but the both are perfectly reasonable. The order switching that this is proposing means that now I can’t just see the list of identifiers being imported at the head of the line, which is just as important, if not more so. Reversing the order also means you’re creating an assign-to-the-right semantic that differs from the rest of the language. On net, the current ordering is technically the best choice here, but the edge is not so extreme as to say that either choice is inherently “wrong”, which is what this article is claiming.

      Get rid of await soup for most code

      Multiple issues here. First: there’s no such thing as an “async function” - async functions are syntactic nicety to make working with promises easier, but any function can return a promise. Second there is the semantic of how you actually implement the continuation points as the absence of ‘await’ means you no longer know where you need to break up the codegen to return a promise vs continuing execution. The use of |await| is also useful for people reading the code as it shows you where the function can be interrupted, otherwise you have to assume every function call can result in the function being halted.

      This reads very much like the author is deciding to intentionally ignore that function lookup in JS is dynamic, and you don’t know at parse/codegen time anything about the call target.

      The easier await alternative

      I have no idea how the author thinks property access works, but there is no way to statically distinguish this new invented syntax from a normal property access. Moreover I fail to understand what makes foo().await superior to await foo()

      Make object-returning arrow functions cleaner

      Or you could just do

      let generatePerson =
        name => ({name, id: generateId()})
      

      Which is not challenging and doesn’t add a pile of syntactic ambiguity, parser complexity, and weird semantics to save two characters.

      1. 4

        Moreover I fail to understand what makes foo().await superior to await foo()

        From the article:

        On top of that, when we combine fluent APIs with this, suddenly we get code that fails the “can be read from left to right” test:

        const VIPs = (await getUsers()).filter(isVIP);
        

        Typing this is often “type getUsers(), await it, parens-wrap it, then add isVIP()”.

        This is definitely something I encounter a fair bit — it’s awkward.

        1. 1

          Ah, is the proposal actually that there’s an additional syntax:

          <expr> . |await| <identifier>
          

          Not saying

          <expr> . |await|
          

          Is now something with different semantics from any other .ident. You could likely avoid some syntactic ambiguity issues when in conjunction with ASI if it was

          <expr> await . <Ident>
          

          Which would have the benefit of fairly consistently extending to

          <expr> await [ <expr> ]
          
          1. 1

            I don’t understand the syntax you’re expressing the syntax here with at all, despite my best efforts! What?

            1. 1

              I’m saying that you logically get an “await.” token, e.g:

              const VIPs = getUsers() await.filter(isVIP);
              

              or

              someThing await["foo"]
              
          2. 1

            It’s slightly awkward but a syntax change that saves one character and removes a potential object property is not a reasonable solution.

            1. 2

              I’m not terribly fussed about the object property argument here — we’re spitballing, so let’s say instead of .await (per Rust) instead it was some other kind of postfix, maybe 💤 for lulz.

              The real benefit isn’t to save a character (also, does it?), but to fix the “inside-out” property that multiple awaits produce. Imagine function calls were similarly denoted with a prefix keyword, call. Instead of:

              function onClearBackgroundColor() {
                  editor.chain().focus().unsetBackgroundColor().run()
              }
              

              We could have:

              function onClearBackgroundColor() {
                  call (call (call (call editor.chain).focus).unsetBackgroundColor).run
              }
              

              Is this example kinda facetious? Sure. But what happens in a month’s time when the library I depend on decide to async-ify half their API? This is a way more plausible future than I’d like:

              function onClearBackgroundColor() {
                  await (await (await (await editor.chain()).focus()).unsetBackgroundColor()).run()
              }
              
          3. 3

            Thanks for reading this and going in depth. I’d like to preface that I threw out these ideas a bit whimsically because I want to hear how people think about them (and don’t seriously consider any of them to be ever adopted), and this is a lot to chew on! You’re totally right about me being a bit blase’ about the consequences, but I do think they’re all technically possible without “breakage” from transitive packages

            You mentioned breaking existing code RE let/var syntax. In my magical universe you are opting into stricter mode at a file level. The internal representation used by JS engines can be unchanged! It’s a “keyword swap”, just one that I believe lands us where we would want to be if we had a do-over (and maybe reserve const for something closer to C++ const for example). But importantly this is at the source level so existing source files would continue to be usable as-is.

            Reversing the order also means you’re creating an assign-to-the-right semantic that differs from the rest of the language. On net, the current ordering is technically the best choice here, but the edge is not so extreme as to say that either choice is inherently “wrong”, which is what this article is claiming.

            Inverting the assignment direction is something I hadn’t thought of! I do think one interesting thing here is how other languages sidestep this with syntax like import my_package.foo (and now you have foo in the namespace), which feels even better to me but less trivial of a change implementation-wise.

            First: there’s no such thing as an “async function”

            Async functions are defined as their own thing in the spec, so while during the initial popularization phase they really were just syntactic sugar they are their own beasts. When they get executed there is special behavior to manage calling into them.

            any function can return a promise

            yes, but async functions are guaranteed to return a promise and are opting into async/await syntax. I think I said this in the original post but I am explicitly opting out of the “normal function returns a promise” case, because that is something that you can determine “statically” for a given function object. You want to be able to know about whether this will require awaiting when calling the function, not on return!

            I also want to point out that I’m not proposing the outright removal of await, simply saying that with my proposed semantics then await is needed much less often. This can be backed up via linters or things like typescript.

            “async functions are just functions returning promises” isn’t true unless you are transpiling as such. I do understand that codegen is a thing, but async functions are not transformed in the ways you are implying when pointing to modern targets.

            You do correctly point out that function lookups are dynamic (as they always are), so one might think that this will mean that each function call in an async function is now having to check a bit. But actually you can do this the other way around: every async function can check its calling context to determine what to do with the generated promise! This is not a clean retrofit by any means, though.

            Ultimately though this was more an idea I wanted to get out of my head.

            but there is no way to statically distinguish this new invented syntax from a normal property access.

            To be clear, obj["await"] would not be considered an await command. What I am saying is a new syntax, and the actual syntax obj.await would have the same semantics as (await obj). This is something at the syntactic level, and not at the “property lookup” level.

            parenthesized arrow function returns are indeed good. I want the best thing but as you point out, it’s two characters.

            1. 1

              You mentioned breaking existing code RE let/var syntax. In my magical universe you are opting into stricter mode at a file level.

              You hit the same problems we had with strict mode. First you break copy/paste for JS, as now you need to copy/paste into identical contexts, then there’s the production use case which is that servers will concatenate responses for performance, and you can run strict code in an non-strict code, but your stricter mode would mean such concatenation would fail to parse.

              which feels even better to me but less trivial of a change implementation-wise.

              The order of terms in an import statement is entirely irrelevant from the PoV of the language implementation, so the only thing that matters is humans reading the code. Hence the questions of language consistency, etc impacting the design decisions.

              Async functions are defined as their own thing in the spec, so while during the initial popularization phase they really were just syntactic sugar they are their own beasts.

              Sorry, badly phrased, the spec has the idea of an async function because it needs to be able to describe the toString behavior and whether yield, etc are valid requires an ability to describe when await and yield keywords are valid, and what the semantics of return is.

              VMs have to convert the JS function in an async function in to a series of internal functions and continuations, based on where await is.

              This change means that every function call has to be turned into yet another continuation, because you can’t know which calls will return a promise until you call it.

              But actually you can do this the other way around: every async function can check its calling context to determine what to do with the generated promise! This is not a clean retrofit by any means, though.

              What exactly is an async function meant to do that is caller dependent? Spontaneously become blocking?

              I do understand that codegen is a thing, but async functions are not transformed in the ways you are implying when pointing to modern targets.

              No, this is exactly how they are transformed. JS runtimes aren’t generating a single piece of code and creating multiple entry points. The wait async codegen works is you create a series of functions with continuations, that’s literally how you implement it.

              What I am saying is a new syntax, and the actual syntax obj.await would have the same semantics as (await obj). This is something at the syntactic level, and not at the “property lookup” level.

              To clarify “await” would become a special and unique token that isn’t treated the same as any other token in JS? Because .identifier is property lookup, JS does not have different semantics for a.b vs a[“b”], e.g a.of is the same as a[“of”].

            2. 2

              I programmed in Python for a long time, and I always hated the import order there. For me, JavaScript’s import order just fits my brain better.

              1. 1

                To me, the whole await thing is the worst thing that could happen, because it instantly colors your functions. I don’t have to deal with that in Lua. Here’s a function that requests a gopher page with blocking calls:

                tcp = require "org.conman.net.tcp"
                
                function get_gopher(host,port,selector)
                  local conn = tcp.connect(host,port)
                  if conn then
                    conn:write(selector,"\r\n")
                    local document = conn:read("*a")
                    conn:close()
                    return document
                  end
                end
                

                And here’s the function I would write if I needed to request a page in a network-event based project:

                tcp = require "org.conman.nfl.tcp"
                
                function get_gopher(host,port,selector)
                  local conn = tcp.connect(host,port)
                  if conn then
                    conn:write(selector,"\r\n")
                    local document = conn:read("*a")
                    conn:close()
                    return document
                  end
                end
                

                If they look the same, it’s beacuse they are, except for the require()—one pulls in the “blocking version” module and the other one pulls in the “non-blocking async version” module. To the programmer, the API is the same, it’s just the implementation is way different underneath the hood and I don’t have to deal with colored functions. Then again, JavaScript doesn’t have coroutines (to my knowledge, I don’t program in it) so maybe that’s why.

                1. 1

                  JS explicitly does not have coroutines, because JS is explicitly single threaded. Retroactively adding concurrent execution would cause many many problems - which makes sense when you recognize that JS is somewhat intrinsically tied to UI, and UIs also tend to be single threaded.

                  That said, I’d argue your example is a good example of why |await| has value: I look at that code, and I can’t tell if it’s blocking or not, I can’t tell if I need to be aware of changes to global state, etc

                  1. 1

                    So is Lua (technically, the C based VM is not thread safe). Coroutines are cooperatively scheduled, there is no preemption, so all data is in a known state. Granted, once a coroutine yields, the state of the program can change, but not, for lack of a better term, unexpectedly with respect to running code. And why do you need to know what blocks and what doesn’t if the system will handle it for you?

                    I see the JavaScript with all the awaits and promises and all I can see is callback hell and hard to follow program flow.

                    1. 1

                      Lua blocking is fine because it’s not used in UI processes. That means randomly making some code block non-visibly is acceptable. Lua also runs only trusted code, so randomly alternating between blocking and non-blocking is acceptable as there’s functionally a single author.

              2. 3

                Your headline is misleading, if not outright clickbait, especially since you admit your ideas are “whimsical”. None of these are about strictness, they’re just syntactic changes that appeal to you.

                An honest headline would have been “Incompatible JavaScript Syntax Changes I’d Like To See.” Who knows, maybe that would have gotten you just as many hits/comments, and it would definitely have shown more respect for your audience.

                1. 1

                  Sorry for wasting your time. I did not choose the title for clicks, but clearly it’s misleading

                2. 2

                  This is mostly syntax sugar, so it could be implemented in a compile-to-JS language. Maybe it’s time for another coffeescript?

                  A really stricter JS should probably look into language semantics, like valueOf, implicit conversions, side effects of loading modules, and monkeypatching of prototypes of fundamental types that can create weird behaviors that make it difficult to accurately reason about potentially-weird JS code.

                  1. 2

                    A lot of this is not increasing strictness, but instead decreasing strictness by adding a heavy dose of sugar. I guess for you letters are hard to type, but I prefer obvious code semantics like writing await to invisible magic. What happens when you have a non-async function return a promise? Nothing is made stricter here. There’s no improvement in VM runtime. Out of the sugars proposed, the only one I like is await as a dotted property.

                    In my opinion, the biggest wart by far in JavaScript today is this scope/binding. Even advanced typescript users easily get it wrong by trying to do someArray.map(object.normalMethod) and ending up with this is undefined errors. If a stricter mode can’t address that, it’s not worth doing.

                    1. 1

                      You are totally right about "stricter mode" being a bad name for what I am saying. It was a jokey way of saying “this is the next iteration of breakage” but clearly not a good fit.

                      What happens when you have a non-async function return a promise?

                      This is part of the deal with what I’m proposing. Of course generic functions can return a promise, as it’s just an object! So instead of checking the return value of every function call (which would generate a hell of a lot of weirdness come refactoring time), the idea is to opt into the async world via the keyword. This has an added bonus of making more and more code proper async functions. Promises are nice but with proper async/await syntax you can avoid building intermediate objects to try and handle arbitrary control flow, as you’re working in a much more restricted environment.

                      I agree that this binding remains extremely fraught. I do not know how you solve someArray.map(object.normalMethod) by itself short of building bound methods (what Python does for this) on function property access. Maybe some new syntactic shortcut to capture the this when you do object.someMethod, combined with making it so that object methods by default raise an error if the defining proptotype for the function isn’t in the prototype chain of the this object? Maybe bound methods are straightforward.

                    2. 1

                      I definitely understand your issues with await, and I believe you struck upon why Rust decided to make it a postfix keyword rather than a prefix. I also believe that the only real reason it’s a prefix keyword is because it (IIRC) comes from C#, and that’s how they did it.

                      The problem that jumps out at me with your approach is that, at least from what I’m seeing, there’s no way to call a function that returns a promise and then not await it. I’ll give you an example of why you might want to do this, though I do concede that it’s a bit of an edge case:

                      async function find(params = {}) {
                        let query = db.from('table').select().is('deleted_at', null)
                      
                        if (params.id) query = query.eq('id', params.id)
                        if (params.sort) query = query.order(params.sort)
                      
                        const { data, error } = await query
                      
                        if (error) throw error
                      
                        return data
                      }
                      

                      In this situation, I want to create a query with a default filter, and then programmatically add to the query if I have certain parameters passed in. It’s possible that no parameters will be passed in, and I will need to run the query and return the data anyway. I can do this pretty easily in current JS, so I think there needs to be some way of telling the promise not to fulfill when it’s returned.

                      Perhaps instead of using await, we could use another keyword to indicate that we do not want to actually fulfill the promise:

                      async function find(params = {}) {
                        let query = defer db.from('table').select().is('deleted_at', null)
                      
                        if (params.id) query = defer query.eq('id', params.id)
                        if (params.sort) query = defer query.order(params.sort)
                      
                        const { data, error } = query
                      
                        if (error) throw error
                      
                        return data
                      }
                      

                      That would certainly make situations where you’re awaiting a promise on every line a lot easier to write and read. But not having that await keyword might remove visibility in situations where the function is gigantic. Obviously this is not something you want to design outright, but if you already have a function that is written in such a way, it’s something that should be accounted for. So IMHO:

                      • await should be optional
                      • The new default with promises is to immediately fulfill them, which is what people normally do anyway
                      • If you don’t want that to happen, you can use defer.