A Declarative replacement for toMethod
Interestingly, two days ago I've imagined the same solution, except I was using .=
instead of mixin
. The .=
syntax is reminiscent to assignment operators like +=
(but, of course, it's not an operator, since its right hand side is not a standalone expression). Reusing your examples:
export const someMixin = obj => obj .= {
method1() {return super.method1()},
Method2() {...},
data: 42
};
and, for classes (but with mandatory class
-keyword):
C .= class {
m2() {super.m2()}
}
PostfixExpression :
[...]
MixinExpression [no LineTerminator here] ++
MixinExpression [no LineTerminator here] --
Is this a typo? What is the point of the ++
/--
postfix operators after
a MixinExpression
?
It's establishing the precedence of the MixinExpression between PostfixExpression and LeftHandSideExpression.
Minor wording nit-pick/question:
In what sense is an operator form declarative? I get that it's not an API -- special form is special ;-). But what's being declared if the only grammar extension is to the expression grammar?
It's establishing the precedence of the MixinExpression between PostfixExpression and LeftHandSideExpression.
Thanks Kevin, I thought it could be the case, but I still can't see when
that is relevant -- as far as I can see, the resulting value of a
mixinExpression is always an object (or a constructor object), and those
would be invalid LHS expressions in regard to the ++
/--
postfix
operations. I assume that BNF grammar is only necessary to establish the
precedence order for expressions that will ultimately throw an error, no? I
feel I'm missing something.
On Feb 23, 2015, at 2:44 PM, Brendan Eich wrote:
Minor wording nit-pick/question:
In what sense is an operator form declarative? I get that it's not an API -- special form is special ;-). But what's being declared if the only grammar extension is to the expression grammar?
I know, one layer declarations are another layers imperative instructions...
What the mixin operator is doing is taking declarative property definitions and dynamically associating them with a specific object or class. The declarative part is the property definitions and home object binding mechanism. No imperative plumbing needed or exposed to make it happen. That's really the contrast I'm making
Allen and I have been discussing this on Twitter. I thought I’d bring my thoughts to list to get them somewhere with less of a character limit.
In general, I think this is a pretty nice syntax for authors. However, I'm concerned that it doesn't satisfy the "metaprogramming" use case that toMethod() solved.
A more abstract and less-motivating way of putting this is to say that mixin
doesn't give us imperative forms to allow us to build a class from the ground up, whereas toMethod() does (or at least takes care of the big issue therein, regarding super
binding).
But I think there are actual use cases at stake here. For example, consider a class-aware counterpart to Bluebird's promisifyAll method. That is, this promisifyClass function would go through a class definition, and for each of its callback-accepting methods, add a promise-returning method alongside it, with a suffix "Async" or similar. This isn't possible in ES6, since we have no way of dynamically adding methods to classes and giving them the correct super
; it isn't possible in Allen's proposal either, from what I can tell.
Allen responds that that sounds like a job for SweetJS or some other preprocessor. And that's a fine response: if we want to admit defeat on class metaprogramming, and say that classes cannot be duplicated except by outputting the correct syntactic forms (whether from a preprocessor or via eval
), then that can be the "imperative API" for classes. But a lot of people like to do their metaprogramming in JS, and it'd be a shame if we said that for classes you have to reach outside the language to get that flexibility.
Is it enough of a big deal to say "let's do toMethod() instead of mixin
"? Probably not. Author ergonomics are much better with the latter. But I'd still like something, whether it be toMethod() or otherwise, that will let me dynamically build a class or augment an existing one.
P.S. I hope we can avoid picking on the specifics of this example too much, but to head off some objections: let's pretend that all callback methods are suffixed with "Cb", and that several of them want to do super references to non-callback/non-promise methods, and hopefully that firms up the use case. The more general point is where the meat is, though.
On Feb 23, 2015, at 3:40 PM, Domenic Denicola wrote:
Allen and I have been discussing this on Twitter. I thought I’d bring my thoughts to list to get them somewhere with less of a character limit.
Just wanted to add that one of the points I tried to make in the tweets was that it is probably good to think about [[HomeObject]] similarly to the way we think about [[Scope]]. There are lots of interesting metaprogramming things you could imagine trying if a function's [[Scope]] as mutable. But the risks it would expose are just too much greater than its utility.
I'm not sure I get it. I am very impressed by Allen's point about using lambda abstraction (Allen said "procedural abstraction") to parameterize a super binding.
Could you show a small self contained example that uses toMethod to do some metaprogramming that cannot be done with Allen's "mixin", where both are without source manipulation? At this point, I care more about the logical issue than whether the example seems well motivated or realistic.
Doh! Nevermind. I get it.
Can you explain what you realized, briefly?
I wonder why we cannot have toMethod, in due course. You had suggested the ES6 signature with a second parameter, with default value:
Function.prototype.toMethod(homeObject, methodName = this.name);
With a function.name spec'ed in ES6, that might be enough to address the concerns raised by @wycats (among other folks).
That if you are given a method with an already bound super, toMethod allows you to create a method like it with a different super binding -- without source manipulation. mixin does not -- again, without source manipulation.
I like Allen's point about [[HomeObject]] being like [[Scope]]. They are both lexically captured. Given people.mozilla.org/~jorendorff/es6-draft.html#sec-function.prototype.tostring and eval, the required source "manipulation" to rebind both is trivial, and does not require parsing or transformation of the source. This has the advantage that, to do any rebinding of lexically captured things, you have to supply rebindings for all of them.
What toMethod does is allow rebinding [[HomeObject]] while preserving the captured [[Scope]]. Is this a bug or a feature?
The old analogy that "toMethod is like bind" does not support this partial rebinding. "bind" binds the this-binding of non-bound, non-arrow functions, which have no lexically captured this-binding. Arrow functions do have a lexically captured this-binding, and bind does not affect it.
If you want to write a mixin or mixin creator, use Allen's abstraction technique intentionally and from the beginning. I think this is good.
On Feb 23, 2015, at 4:24 PM, Brendan Eich wrote:
I wonder why we cannot have toMethod, in due course. You had suggested the ES6 signature with a second parameter, with default value:
Function.prototype.toMethod(homeObject, methodName = this.name);
With a function.name spec'ed in ES6, that might be enough to address the concerns raised by @wycats (among other folks).
I didn't get into naming issues in this proposal, because it is either (depending upon perspective) a parallel or orthogonal issue.
For a long while functions had a [[MethodName]] internal slot specifically to support property name defaulting in super
property references and the second argument for toMethod was used to set [[MethodName]]. I don't thing having =this.name as the default was ever in the spec. It's semi-plausable, but during that era, not every super referencing function had a useful 'name' property value.
If we needed to restore [[MethodName]] because we agreed to bring back implicit super() current method name qualification the mixin operator would handle it, an a parallel manner to what it does for [[HomeObject]]. (but the remember, the big concerns about that feature weren't related to how the name was acquired)
Note that the lambda abstraction technique also can support a dynamically provided method name:
let aVarNamedMixin = (obj, name) => obj mixin {
[name] () {
//do something
super[name]()
}
}
A programmer who wrote the assignment aPusher.push = MyArray.prototype.push was probably thinking that they could just reuse the push method from MyArray.prototype and that the super.push call within it would start searching for a push method at the [[Prototype]] of aPusher. But it doesn't.
As a programmer - I wouldn't think it'd make that call. I don't think that
it's safe to say most JS developer would expect super
to call Pusher
here. JS programmers are used to dynamic this
and ad-hoc mixins but I
don't think they would expect dynamic super
when "borrowing" methods like
that. I think people view this
as context and super
as an alias but
time will tell.
Care to share more interesting use cases for mixin
?
Allen Wirfs-Brock wrote:
Note that the lambda abstraction technique also can support a dynamically provided method name:
let aVarNamedMixin = (obj, name) => obj mixin { [name] () { //do something super[name]() } }
That's not bad. Never reach for eval where a function will do.
Still, if we can have an API, then all else equal we should.
What about this? Would this satisfy the metaprogramming problem?
Object.getOwnPropertyNames(obj)
.map(name => [name, obj[name]])
.filter(keepCorrectFunctions)
.map(([name, f]) => {
obj mixin {
[name + 'Async'](...args) {
return new Promise((resolve, reject) =>
f.call(this, ...args, (err, ...rest) =>
err == null ?
resolve(rest) :
reject(err)));
}
}
});
Here is a new proposal for a declarative replacement for Function.prototype.toMethod which was dropped from ES6 earlier this year allenwb/ESideas/blob/master/dcltomethod.md
I've added this to the agenda for next months TC39 meeting but pre-meeting discussion is welcomed right here.