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

Partial application plugin #9474

Merged
merged 16 commits into from Mar 13, 2019

Conversation

byara
Copy link
Contributor

@byara byara commented Feb 8, 2019

Q                       A
Fixed Issues? Fixes #9342
Patch: Bug Fix? No
Major: Breaking Change? No
Minor: New Feature? Yes
Tests Added + Pass? Yes
Documentation PR Link
Any Dependency Changes?
License MIT

This PR addresses the proposal regarding Partial Application plugin.

The syntax rules that was suggested in the proposal to be covered:

    • Given f(?), the expression f is evaluated immediately.
    • Given f(?, x), the non-placeholder argument x is evaluated immediately and fixed in its position.
    • Given f(?), excess arguments supplied to the partially applied function result are ignored.
    • Given f(?, ?) the partially applied function result will have a parameter for each placeholder token that is supplied in that token's position in the argument list.
    • Given f(this, ?), the this in the argument list is the lexical this.
    • Given f(?), the this receiver of the function f is fixed as undefined in the partially applied function result.
    • Given f(?), the length of the partially applied function result is equal to the number of ? placeholder tokens in the argument list.
    • Given f(?), the name of the partially applied function result is f.name.
    • Given o.f(?), the references to o and o.f are evaluated immediately.
    • Given o.f(?), the this receiver of the function o.f is fixed as o in the partially applied function result.
    • Given f(g(?)), the result is equivalent to f(_0 => g(_0)) not _0 => f(g(_0)). This is because the ? is directly part of the argument list of g and not the argument list of f.
  • I'm not 100% sure about 5, 6, 7 and 8 and how to address them.
  • I welcome any suggestions and reviews to improve this, I'm doing this as part of my thesis and anything that could teach me something is great.

@babel-bot
Copy link
Collaborator

babel-bot commented Feb 8, 2019

Build successful! You can test your changes in the REPL here: https://babeljs.io/repl/build/10466/

@nicolo-ribaudo nicolo-ribaudo added PR: New Feature 🚀 A type of pull request used for our changelog categories Priority: Low Spec: Partial Application labels Feb 8, 2019
@nicolo-ribaudo
Copy link
Member

(I'm marking this as Low priority until the parser PR is merged - don't worry! 😉 )

t.variableDeclaration("const", [
t.variableDeclarator(
receiverLVal,
t.identifier(receiverRVal(node)),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use path.scope.push to generate those vars and avoid the iife:

const g = o.f(?, x, 1);

// becomes

var _receiver, _func, _param, _param2; // generated by scope.push
const g = (_receiver = o, _func = o.f, _param = x, _param2 = 1, _arg => _func.call(_receiver, _arg, _param, _param2);

Btw, there is probably some scope/types helper function that says that 1 is a pure constant value and can thus can be transpiled as

const g = (_receiver = o, _func = o.f, _param = x, _arg => _func.call(_receiver, _arg, _param, 1);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to ask for a bit of clarification:

  • what is the advantage of not using the iife?
  • could you explain how pushing to scope creates the declarations for me?

Copy link

@danielcaldas danielcaldas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a left a few minor comments that might make some parts of the implementation more compact. Great job man! 👍

}

/**
* a recursive function that unfolds MemberExpressions within MemberExpression

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a rewording to a recursive function that unfolds nested MemberExpressions

return true;
}
}
return false;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe refactor to something like:

function hasArgumentPlaceholder(node) {
  return node.arguments.some(t.isArgumentPlaceholder)
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is* functions can accept two arguments, so you'll need to wrap it in an arrow to only use one parameter (or use partial application lol)

function receiverRVal(node) {
let rVal = unfold(node).split(".");
rVal.pop();
rVal = rVal.join(".");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can return immediately rVal.join(".");

* @returns {Array<Expression>}
*/
function unwrapArguments(node) {
const nonPlaceholder = node.arguments.filter(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can also return immediately here.

*/
function unwrapAllArguments(node, scope) {
const clone = t.cloneNode(node);
clone.arguments.forEach(argument => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe refactor into a .map

*/
function argsToVarDeclarator(inits, scope) {
let declarator = [];
declarator = inits.map(expr =>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also return immediately here return inits.map...

* @param {Array<Arguments>} args
*/
function mapNonPlaceholderToLVal(nonPlaceholderDecl, allArgsList) {
const clone = Array.from(allArgsList);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Array.from(allArgsList)
  .map(cl => ...);

@byara
Copy link
Contributor Author

byara commented Feb 13, 2019

@nicolo-ribaudo @danielcaldas thank you for all the reviews, I'll try to address them as soon as possible.

Meanwhile, I have a question:
Regarding this syntactic rule of the proposal:

  1. Given f(?), the name of the partially applied function result is f.name.

What do you think about this solution:

// if we have this:
const foo = bar(1, ?, 3, ?);

// currently converted to this:
const foo = (() => {
  const _func = bar;
  const _param = 1,
        _param2 = 3;
  return (_argPlaceholder, _argPlaceholder2) => _func(_param, _argPlaceholder, _param2, _argPlaceholder2);
})();

bar.name; // => bar
foo.name; // => empty

Instead we do this:

const foo = (() => {
  const _func = bar;
  const _param = 1,
        _param2 = 3;
{
  const bar = (_argPlaceholder, _argPlaceholder2) => _func(_param, _argPlaceholder, _param2, _argPlaceholder2);
  return bar;
}
})();

bar.name; // => bar
foo.name; // => bar

or maybe we can get the foo.name to be foo?

@nicolo-ribaudo
Copy link
Member

Can we use normal functions?

const add1 = add(1, ?);

// ->

var _func, _param;
const add1 = (_func = add, _param = 1, function add(_placeholder) { return _func(_param, _placeholder) });

@byara
Copy link
Contributor Author

byara commented Feb 13, 2019

Can we use normal functions?

const add1 = add(1, ?);

// ->

var _func, _param;
const add1 = (_func = add, _param = 1, function add(_placeholder) { return _func(_param, _placeholder) });

It works.
That's a good idea and it looks better. I'll change the plugin accordingly.

t.variableDeclaration("const", [
t.variableDeclarator(
receiverLVal,
t.identifier(receiverRVal(node)),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what receiverRVal (which, btw, doesn't generate a valid identifier since the result can contain .) is needed for: isn't using node.callee (and node.callee.property for functionLVal) enough?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, it doesn't work:

a.b.fn().c["d"].e(?);

// ->

(() => {
  const _receiver = a.b.fn.c.undefined;
  const _func = a.b.fn.c.undefined.e;
  return _argPlaceholder => _func.call(_receiver, _argPlaceholder);
})();

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, and instead of using newFuncLVal/newReceiverLVal you could use path.scope.generateUidIdentifierBasedOnNode(node.callee) and path.scope.generateUidIdentifierBasedOnNode(node.callee.object). It's up to you to decide.

object.get().fn(?);

// ->

(() => {
  const _object$get = object.get;
  const _object$get$fn = object.get.fn;
  return _argPlaceholder => _object$get$fn.call(_object$get, _argPlaceholder);
})();

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, and the previous example should be something like this, otuherwise we might execute some accessors twice:

(() => {
  const _object$get = object.get;
  const _object$get$fn = _object$get.fn;
  return _argPlaceholder => _object$get$fn.call(_object$get, _argPlaceholder);
})();

const h = (_p = p, _p$b = p.b, _param2 = y, _param3 = x, function b(_argPlaceholder2) {
return _p$b.call(_p, 1, _param2, _param3, 2, _argPlaceholder2);
});
const j = (_a$b$c$d$e = a.b.c.d.e, _a$b$c$d$e$foo = a.b.c.d.e.foo, function foo(_argPlaceholder3) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be

const j = (_a$b$c$d$e = a.b.c.d.e, _a$b$c$d$e$foo = _a$b$c$d$e.foo, ...

because a.b.c.d.e could be getters which must be accessed exactly once.

* @param {Array<Node>} args
*/
function mapNonPlaceholderToLVal(nonPlaceholderArgs, allArgsList) {
const clonedArgs = Array.from(allArgsList);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need to clone this array? You are only cloning the array, not its elements.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are correct, this is useless. I removed it.

*/
function mapNonPlaceholderToLVal(nonPlaceholderArgs, allArgsList) {
const clonedArgs = Array.from(allArgsList);
clonedArgs.map(arg => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: you can use forEach

let cloneList = [];
allArgsList.forEach(item => {
if (item.name && item.name.includes("_argPlaceholder")) {
cloneList = cloneList.concat(item);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cloneList.push(item)

node.callee,
);
const receiverLVal = path.scope.generateUidIdentifierBasedOnNode(
node.callee.object,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should only be generated when node.callee is a member expression.

@nicolo-ribaudo
Copy link
Member

Does ...spread work?

foo(a, ...b, ?);

Since the spec doesn't define how it should behave (or I can't find it), we can just throw an error when we find it. Or maybe it uses one of the following semantics? (cc @rbuckton)

var _foo, _a, _b;
_foo = foo, _a = a, _b = b, function foo(_arg) { return _foo(_a, ..._b, _arg) }

// or maybe it calls [Symbol.iterator] earlier

_foo = foo, _a = a, _b = [...b], function foo(_arg) { return _foo(_a, ..._b, _arg) }

@rbuckton
Copy link

@nicolo-ribaudo: Anything usable in a normal function call should be usable here. All evaluation should be eager, so the second example is correct.

@nicolo-ribaudo
Copy link
Member

(From the original post)

  • I'm not 100% sure about 5, 6, 7 and 8 and how to address them.

Don't they already work?

@byara
Copy link
Contributor Author

byara commented Feb 16, 2019

(From the original post)

  • I'm not 100% sure about 5, 6, 7 and 8 and how to address them.

Don't they already work?

I added a few tests for 7 and 8. Regarding 5, I think, I sort of understand what "lexical this" means.
For 6, I don't think I understand the "this receiver" for f(?) 😅
For other reviews, I'll address them ASAP.

@rbuckton
Copy link

Ignoring partial application for a second, when you call f() in strict mode, the receiver (the value used for this inside of f) is undefined. When you call o.f(), the receiver is o. When you do f.call(o), the receiver is o.

6 basically means that when you do g = o.f(?); g(), the value o is preserved as the receiver.

@byara
Copy link
Contributor Author

byara commented Feb 16, 2019

Ignoring partial application for a second, when you call f() in strict mode, the receiver (the value used for this inside of f) is undefined. When you call o.f(), the receiver is o. When you do f.call(o), the receiver is o.

6 basically means that when you do g = o.f(?); g(), the value o is preserved as the receiver.

@rbuckton Thank you for the explanation. I think 6 is also addressed.

@nicolo-ribaudo nicolo-ribaudo added this to the v7.4.0 milestone Feb 18, 2019
remove unnecessary error message and hasPartial function from parseNewArguments

add types for PartialExpression

Update the tests

rename PartialExpression to Partial

move Partial from expressions to types and rename to ArgumentPlaceholder

add tests for ArgumentPlaceholder in babel-generator

rename Partial to ArgumentPlaceholder

update the tests

remove alias from the type and undo changes in generated folder

adds a nice error message

better definition for the type

auto-generated files

update the conditional for allowPlaceholder message and tests

update CallExpression definition to accept ArgumentPlaceholder

change description

clean up

indent ArgumentPlaceholder entry and revert unwanted changes
@byara byara force-pushed the partial-application-plugin branch 3 times, most recently from e4b72a9 to 9196d67 Compare February 20, 2019 10:50
@byara
Copy link
Contributor Author

byara commented Feb 20, 2019

@nicolo-ribaudo one of the builds for travis times out. Would you retrigger it?

@nicolo-ribaudo
Copy link
Member

It seems that after the rebase the plugin disappeared? If you need it, you can restore from https://github.com/nicolo-ribaudo/babel/tree/pr/byara/9474-backup (it is 6 days old so commits after #9474 (review) are not included). If you prefer, I suggest using git reflog to restore the lost commits.

if (t.isArgumentPlaceholder(arg)) {
const id = scope.generateUid("_argPlaceholder");
placeholders.push(t.identifier(id));
args.push(t.cloneNode(t.identifier(id)));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to cloneNode here, t.identifier returns a new node.

@nicolo-ribaudo
Copy link
Member

The failing tests are fixed by #9558

{
"name": "@babel/plugin-proposal-partial-application",
"version": "7.2.0",
"description": "Transform pipeline operator into call expressions",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be updated to match the readme.

Copy link
Member

@nicolo-ribaudo nicolo-ribaudo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉

@nicolo-ribaudo
Copy link
Member

nicolo-ribaudo commented Mar 4, 2019

Could you rebase (or merge master) this PR, since the parser PR has been merged?

Copy link
Member

@danez danez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work.

Does this work with awaiting async functions? Can we add a test for this? Thinking about it more, I guess that the partial application does not really touch with async functions as await foo(?) is nonsense. await foo(?)(1) could work, but doesn't make much sense. So not sure if a test makes sense.

Also it would be nice to add a test where the result from the call is not assigned but directly chained: foo(?, a)(b).then(()=>{}); Doesn't make much sense either, but I guess it should work.

@@ -0,0 +1,3 @@
{
"plugins": ["proposal-partial-application"]
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: The options.json files are the same for all tests as I can see, so the config file could be moved on level up into the general folder. Then all the fixtures in general would share the config.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@danez Thanks for the feedback.
I moved options.json one level up and added a new test.

@nicolo-ribaudo nicolo-ribaudo added the PR: Needs Review A pull request awaiting more approvals label Mar 7, 2019
compare(a, b) {
if (a > b) {
return a;
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit, but this semi seems unnecessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, unnecessary, I'll remove it.

false,
),
]);
path.replaceWith(finalExpression);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not big deal by any means, but since both of these end with replacing the path with a sequenceExpression, we could do:

const sequenceParts = [];

if (node.callee.type === "MemberExpression") {
  sequenceParts.push(
    /* stuff */
  );
} else {
  sequenceParts.push(
    /* stuff */
  );
}

path.replaceWith(t.sequenceExpression(sequenceParts));

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea! made the necessary changes.

Copy link
Member

@existentialism existentialism left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benefit of coming late to the party is the code looks awesome already :)

Nice work!

@nicolo-ribaudo
Copy link
Member

Awesome work!

@nicolo-ribaudo nicolo-ribaudo merged commit 29cd27b into babel:master Mar 13, 2019
mAAdhaTTah added a commit to mAAdhaTTah/babel that referenced this pull request Mar 15, 2019
* master: (58 commits)
  Remove dependency on home-or-tmp package (babel#9678)
  [proposal-object-rest-spread] fix templateLiteral in extractNormalizedKeys (babel#9628)
  Partial application plugin (babel#9474)
  Private Static Class Methods (Stage 3) (babel#9446)
  gulp-uglify@3.0.2
  rollup@1.6.0
  eslint@5.15.1
  jest@24.5.0
  regexpu-core@4.5.4
  Remove input and length from state (babel#9646)
  Switch from rollup-stream to rollup and update deps (babel#9640)
  System modules - Hoist classes like other variables (babel#9639)
  fix: Don't transpile ES2018 symbol properties (babel#9650)
  Add WarningsToErrorsPlugin to webpack to avoid missing build problems on CI (babel#9647)
  Update regexpu-core dependency (babel#9642)
  Add placeholders support to @babel/types and @babel/generator (babel#9542)
  Generate plugins file
  Make babel-standalone an ESModule and enable flow (babel#9025)
  Reorganize token types and use a map for them (babel#9645)
  [TS] Allow context type annotation on getters/setters (babel#9641)
  ...
@lock lock bot added the outdated A closed issue/PR that is archived due to age. Recommended to make a new issue label Oct 4, 2019
@lock lock bot locked as resolved and limited conversation to collaborators Oct 4, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
outdated A closed issue/PR that is archived due to age. Recommended to make a new issue PR: Needs Review A pull request awaiting more approvals PR: New Feature 🚀 A type of pull request used for our changelog categories Spec: Partial Application
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Partial Application syntax: Stage 1
7 participants