Hacker News new | ask | show | jobs
by dustin1114 4113 days ago
I think I like most of what this document proposes except for the following:

  In strong code, accessing objects (strong or not) throws on missing properties.
  New object properties have to be defined explicitly and cannot be removed
  from strong objects.
To me, this seems to break a fundamental aspect of the language. I've found it very acceptable to be able to define an object literal property "on the fly." However, with strong mode trying to make the language friendlier to eventually being more statically typed, I see the necessity. It just boggles my dynamically typed mind :-)
5 comments

It does break fundamental uses of the language. Throwing on missing properties is a near-hostile change for most current JS developers.

The given justifications for doing this, though, seem to be all about performance rather than an attempt to evolve the language.

If that's true, I think it's possible the proposal has a naming problem.

Calling it "use strong" implies that this is about evolving the JS language so that devs are spending time writing more strongly-typed code.

Calling "use optimize" would make it a lot clearer that this is not an attempt to Java-ify JS, and this is more something you'd primarily invoke for performance-critical code paths.

I fail to see how it breaks anything? It is opt-in. Aside from that "use optimize" is indeed a better naming.
It is supposed to be backwards compatible.

    "use strong"
    try {
        foo = bar[maybeMissing]
    } catch {
        // only runs in strong mode
    }
The above will take different code paths depending on whether your JS engine supports "strong" mode.
That's a good spot.

I suppose it's a lot easier to declare one's intention to create a backwards-compatible subset than to actually create one.

Hope the V8 people are open to revisions on the details at least.

This is true of strict mode as well, which shipped in ES5.

    "use strict";
    try {
      a = true;
    } catch(e) {
      // only runs in strict mode
    }
It's true of any mode switch that makes the language smaller. You shouldn't write non-strict (or non-strong) code if you opted into that mode, and catching those errors defeats the purpose of using it.

But it's not only true of mode switches...

    try {
      JSON.parse("{}");
    } catch(e) {
      // only runs in browsers that don't support JSON.parse
    }

    try {
      [ 1, 2, 3 ].forEach(function(x) { /* ... */ });
    } catch(e) {
      // only runs in browsers that don't support forEach
    }
Or even just:

    const x = 10;
    // only runs in browsers that support const
Any language change can cause differences between what executes in one browser vs. what executes in another. What strong mode guarantees is that if your code doesn't throw errors in strong mode, it won't throw errors in non-strong-mode (which is more than many changes guarantee!). Any other kinds of compatibility guarantees are impossible to make unless your changes are literally meaningless.
Indeed (regarding your first point), my bad. I hadn't read the complete proposal, just the SaneScript slides, where that point was not fully developed. From TFA, emphasis is mine:

However, a mode directive has the significant advantage that any program +not hitting any of the strong mode restriction+ should run unchanged in a VM not recognising the directive, and no translation step should be required.

Your next two points are different: the standard library additions can be polyfilled and the syntax change is intentionally backwards incompatible.

That's funny; I think that's the only part of what the document proposes I like. -- Well, okay, that's not really true; there are a lot of bits that just make sense, either because they hurt performance for little benefit (holes in arrays) or they're just dumb (arguments.caller). But I don't like gratuitously locking things down in a way that makes highly dynamically-typed code harder to write, where it doesn't seem to solve a real unavoidable performance problem: for example, the ban on constructors leaking 'this', and the oddly specific recursion limitations. In general, the document seems to express the sentiment that only statically typed code matters, which I think is short-sighted. I also dislike, among other performance-unrelated changes, the "let's fix C" syntax bits, such as banning fallthrough, which is just likely to annoy programmers familiar with C (who are used to writing code that relies on it on occasion).

However, making nonexistent property accesses silently return undefined is just an amazing way to ensure typos in property names never get caught. I don't think `foo.bar || baz` is much to sacrifice - Python, for example, has getattr(foo, 'bar', baz), which works fine, and has the benefit of returning foo.bar if it exists at all, not just if it's a truthy value.

My experience is that the python way results in long and obfuscating chains of access checks. It's a fine idea to not make it the default behaviour, but there should be a way to do a chained check a-la coffeescript's '?.' to ease deep accesses, especially since in js objects are commonly used for data structures.
I've found that even in Python, only statically typed code matters - at least, statically enough that I can be confident that it works. I don't think the ability to have an object also be a map is especially valuable; code where everything is one or the other is clearer. These changes won't affect the ability to monkeypatch methods on individual instances (that's the one "really dynamic" thing that I find actually useful), will they?
It's kind of funny. ES6 seems to be making JS more "Python", while "strong mode" makes it more like Java/C# (at least to me). I'm open to worthwhile changes; I'm just thinking about the tens of thousands of lines of JS I've written with various utilities and libraries, and how they will eventually fit into ES6...I'm not too sure about strong mode, though. I guess we'll see how it all pans out.
Being able to do

  foo = bar.x || 3;
rather than

  foo = bar.hasOwnProperty('x') ? bar.x : 3;
is nice.
Personally, I would say

    foo = ('x' in bar) ? bar.x : 3;
instead. The problem with your code is that if the property bar.x exists, but is one of any number of values, like 0 or false, your code will still set foo to 3. Requiring properties to be explicitly created means that you're separating existence from value, which are two very different things in my book.
Which are some of the most insidious bugs possible in JS. I shudder every time I see the syntax in your post's parent. It's a red flag. Your solution is good. I also use a lot of typeof(bar.x) !== 'undefined' since it's very explicit.
Every time?

I mean, very many times (for example) you're pulling in some JSON from a server app, which will have types properly enforced at the database and/or application level. There are still gotchas around defaulting to true, whether or not an empty string is a valid value and so on, but there are many cases where foo || bar is safe enough.

I have to disagree with you there. It's simply not a good idea to have a works-sometimes syntax for "if property is unset". The brevity doesn't make up for the fact that you sometimes have to fallback on the explicit check anyway. It may look clever but it's an abusage, and you're eventually going to get a production error, unless you're testing for it, and if you've got tests for that, brevity has already lost.
Well, I'm not responsible for your code, so you do whatever you want. But this, for me, is like being Van Halen and seeing brown M&Ms in the bowl. It's a red flag when I see that syntax in code that I should be much more defensive about what's happening everywhere else. There are no assurances about data, especially about data coming across the network.
Agreed. And unless I'm reading the document wrong, this would also be prohibited in strong mode:

  let x = {
    keyA: "valueA"
  };
  x.keyB = "valueB";
I think they're trying to push onto [Maps].

[Maps]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...

Yep, they're explicitly trying to do that.

For what it's worth, you can still use "options hashes" in your argument APIs in strong mode using objects; you just write a library function something like:

    function options(options, defaults) {
      // the final args start off as a clone of the default args
      let args = Object.clone(defaults);

      // we then loop through the keys and copy in any overrides
      for(let key of Object.keys(args)) {
        // ignore inherited properties and skip missing ones
        if(args.hasOwnProperty(key) && options.hasOwnProperty(key)) {
          args[key] = options[key];
        }
      }

      // args now has all of the overrides from options
      return args;
    }
And then in all your functions that take options objects:

    function bakeBread(ingredientOverrides) {
      let ingredients = options(ingredientOverrides, {
        flourType: 'whole wheat',
        sugarAmount: '3 tbsp',
        waterAmount: '1 cup',
        milkAmount: '0.3 cups',
        flourAmount: '4 cups'
      });

      let batter = mix(ingredients);
      return bake(batter);
    }
JS is still quite dynamic, even in strong mode — you can define arbitrary objects and types at runtime, and easily inspect/reflect on them — it's just a little harder to silently corrupt data.

The neat thing about using named arguments with objects is that in typed variants of JS — for example, TypeScript, or perhaps someday SoundScript — you can actually typecheck them! Maps can't do that in any language I know of: by design they can contain anything.

The Java (5+) Map interface can be typed using generics (which -- if JS eventually introduces static typing -- I hope is implemented).

  Map<String, Integer> = new HashMap<String, Integer>();
That declares a Map with a String key and Integer value. Is this what you're thinking of?
Not what I was thinking of, but I should've been more explicit. Using generics you can type the keys and values of Maps in any language with generic support that I'm familiar with: certainly C++ and Java can. But you can't make type assertions about certain values existing under certain keys: that's what structs/classes/etc are for. But, those constructs can't easily be generated at runtime, and are typed nominally: even just as a caller you have to explicitly say they inherit (or in Java, implement) a specific type. However, with TypeScript-like structural subtyping, you can do:

    interface BreadIngredientOptions {
      flourType?: String; // this is the syntax for optional strings
      sugarAmount?: String; // ditto: it's the ? that makes it optional
      // ...
    }

    function bakeBread(ingredientOverrides: BreadIngredientOptions) {
      // ...
    }

    // callers don't need to explicitly inherit or implement to be type checked
    // however, since all properties are optional, this is less interesting
    bakeBread({
      flourType: 'white'
    });
But you can do even better than that example shows. One common problem with maps-as-named-arguments is that you can't easily determine which arguments are required and which are optional. With typed optional properties and structural subtyping you can enforce that at compile time, as follows:

    interface MyArgumentInterface {
      requiredArg: number;
      optionalArg?: number;
    }

    function f(args: MyArgumentInterface) {
      // ...
    }

    // This works:
    f({
      requiredArg: 10,
      optionalArg: 5
    });

    // This also works:
    f({ requiredArg: 50 });

    // This statically throws at compile time:
    f({ optionalArg: 10 });
It's a combination of the simple object literal syntax from raw JS that makes it easy to create objects of arbitrary types, with structural subtyping. I'm not aware of any language with the same features (but would love to be corrected!).
I'm torn here. I agree that this is one of the Nice Things about javascript - I can do stuff like

  let foo = opts.foo || 'default'
Which is nice. On the other hand, if you look at how V8 does its JIT compiling, it seems like there's just some things you can't optimize around, and they've gotten as far as they can reasonably be expected to get there. Having object schema that can change on the fly is just really hard to JIT efficiently.

My worry is that Strong Mode becomes the defacto standard, and we end up losing the flexibility and expressiveness of well-written JS, and end up with static typing all over. If your JS is compact and otherwise well-formed, you can probably afford the compiler hit sometimes, knowing that code is a lot easier to write.

I agree and I come from a strongly typed background (.Net). It is extremely liberating and useful to just add properties to an object at any time, even ones I didn't create. Kind of like slapping a sticker on something for later reference, whereas a strong types object just explodes when apply you the sticker.

I understand that there are performances issues to doing things this way, but it seems like you could have the strong typed base while still allowing expando properties that may be slower? In .Net the added Dynamic but I find it of limited use because it must be explicitly used rather than just Object allowing it which is what you are going to get from most libraries.

I don't think performance is the primary reason behind this. The motivation section in the article states in the very first sentence:

Silent property failures and the resulting proliferation of 'undefined' are the most prominent mistake in JavaScript, and can be rather tedious to debug (very much like null pointer exceptions in other languages, but much more common).

Having different objects (that were created in the same way and represent the same thing) potentially have different properties at different parts of their lifetime can easily lead to a codebase where you can never be certain of anything about such objects and have to do loads of explicit checking every time you use them. Especially in a larger codebase developed by more than one person.

It's a nice feature for whipping up some quick prototypes though.

It's not just for quick prototypes though, many times for me it's about marking up objects from other libraries with my own data. I'm extending them one off and not changing the base behavior in any way, this should cause no issues to anyone else using that object. Yes that can be done with inheritance in say C# so long as the base object isn't sealed, which many library classes are from MS especially, ugh.

The undefined issue seems no different to the null reference as you mentioned. I run into it constantly in C#, in fact everyone does that why they are adding the null lifting operator (?.), javascript could do the same for both undefined and null.

I may be weird but I actually like the fact that javascript has both undefined and null. It allows an extra state over just null in C# which I constantly wish I had, typically for things like data/domain objects. With undefined and null it's trivial to encoded the fact that a property is not loaded say from a db or sent from a client vs it being loaded but the value is null or sent over the network with a null value.