Backwards compatible optionally named arguments

Inspired by the previous thread on Optional Named Parameters...

function stringify(data, ?{replacer, indent} ) {...}
// These are the same:
stringify(myData, undefined, 2)
stringify(myData, {indent: 2})
stringify(myData, undefined, {indent:2}) // (though this one is silly))
stringify(myData, {indent: 2, replacer:undefined}) // as is this
// These aren't
stringify(myData,x=>x+1,{indent:2,replacer:undefined}) // The positional value dominates
stringify(myData,{replacer:undefined},{indent:2}) // The non-final argument is not destructured

When a function declaration uses the ?{} notation and is called with the last argument being an object, that object is deconstructed over the arguments that have not been assigned positionally.

Using this with arguments that will ever be objects is discouraged, though you can still pass them in named mode, so a function that takes objects as those arguments on rare occasions might still use this feature. In order to avoid bugs in this scenario, we might raise an exception on unused keys in the passed object.

This has all the usual advantages of optionally named arguments, allowing programmers to consider clarity and compactness in the context of the code they're actually writing. This is familiar to us from python.

But by being explicit, it means that a function's author gets to decide whether name, ordering or both of arguments is part of the signature, so they won't break users by accident. Also, we're using standard destructuring that is already well understood.

You may have recognized the stringify function from my example. I got it from the JSON library. If it were being designed today, it would probably take the latter two arguments as named, but it predates object destructuring and changing the api would break far too much code. But shifting it to optional named arguments in this way allows old code to keep working and new code to use clearer calling.

There's a lot of functions in the JS standard library that would benefit from a non-breaking conversion to named arguments.

I understand why you need to make this restriction, but there are some issues with it.

function required(data, { constructor }) { return constructor; }
required("", { dummy: 0 }); // returns [Function: Object]
required("", []); // returns [Function: Array]
function optional(data, ?{ constructor }) { return constructor; }
optional("", { dummy: 0 }); // throws because "dummy" unused?
optional("", []); // non-enumerable "length" unused, does it throw?
optional("", {}); // ???

Does the last line return the constructor from prototype? If so, why does it not throw because of unused properties on prototype? And if unused inherited properties are silently ignored, what if you later add a named parameter matching a previously ignored inherited property?

I think this restriction would lead to some surprising behaviour, unless you completely ignore the prototype, i.e. only consider own properties both for binding and for checking unused.

Hmm. This actually gets trickier than I'd realized. I hadn't been thinking about prototypes at all...

I used the term "not an object". That isn't actually a javascript concept. let {toString} = 42; is valid javascript. So when should we regard a terminal argument as nonobject for nondestructuring purposes?

Really, if you write

function foo(?{data, length}) {...}

I would expect data=[1,2,3] and length=undefined, not an attempt to destructure an array as an object.

I think the rule should be that only objects with empty prototypes get destructured for optionally named arguments. This is a little surprising, since js generally doesn't make that distinction, but it lets us use generic destructuring, and I think it Does What We Mean in approximately all cases.

What is an "empty prototype"? Most objects don't have a null prototype, if that's what you mean. What about something that extends an object that in turn extends null?

There's a lot of destructuring use cases that need prototype chain lookup - there's a reason destructuring uses Get() instead of grabbing an own property.