jhusain / compositional-functions

138 stars 7 forks source link

Advantages over yield and synchronous use cases #3

Open benjamingr opened 9 years ago

benjamingr commented 9 years ago

I think it would be really beneficial in this proposal to explain what this does that yield does not enable you to do at the moment. As is, why would you want compositional functions given you can already do this in not-so-bad syntax with yield.

I think it might also be very beneficial to expand this proposal (without changing semantics) and explain how this can be used for arbitrary decorators.

Here are some interesting use cases that can be implemented with compositional functions and enjoy this syntax.

class Foo {
    observable foo(){
         // converts any return value to an Observable.just and any thrown error to a rejected
         // observable to guard against double try/catching 
    }
    promise bar(){
         // same idea but with promises, converts throws to rejections and returns to fulfillments
    }
    cached baz(){
         // this method's return value is memoized
    }
    synchronised boo(){
         // method returns a promise/task/observable and guarantees calls to underlying fn are queued 
    }
    remote ban(){
          // this method executes a remote procedure call to a server
    }
    async animal(){
         // normal promise returning fn
    }
    logged cow(){
        // every call to this method is logged
    }
    timed chicken(){
         // this method is benchmarked
    }
    contract(x => !Number.isNaN(x) && x > 0) factorial(num){
         // this contract is enforced in runtime
    }
    contract(Boolean) doSomethingToTruthy(){
         // enforces truthy arg in runtime
    }
    contract(x => x., ret => ret instanceof Observable) thisAlwaysReturnsObservables(){
        // enforces return value
    }
    autocurry add(x,y){
          // automatically curried
    }
    compose(autocurry, observable, contract(Boolean) map(fn, val){
        // returns a carried version of the map that allows awaiting observables
        // and ensures a non-null argument is passed for fn
    }

}

I think the fact we can get arbitrary decoration in class declarations and expressions should not be understated here and that there are a lot of interesting use cases to show.

Also, I wonder why not add async as a global which makes this proposal completely possible on top of the async/await one and can streamline it.

bmeck commented 9 years ago

we should discuss this some as some things become confusing like async cached bar() would be both using the async composer and the cached composer?

benjamingr commented 9 years ago

@bmeck not to mention async cached and cached async would mean different things. It would definitely be an interesting issue to discuss how compositional functions... compose.

jhusain commented 9 years ago

As for the question of why we don't just do this with yield, the committee has already decided that the await syntax is desirable enough to pursue because of the status of async/await. This proposal simply aims to make that composition mechanism generic.

Although composition functions are flexible enough to be used for any type of decoration, the intention is that they used to compose different scalar asynchronous primitives.

My personal opinion is that if you want decoration on any function, we should do that within the context of the decorator proposal. Composition functions are different in that they activate the await syntax, which does not appear to be necessary for many of the examples above. I'm a big fan of decorators and would like to see them used for the use cases you outlined.

benjamingr commented 9 years ago

@jhusain wouldn't it be preferable to kill the two birds in one stone?

Like you said, these things are powerful enough for it. The syntax they give works and is frankly really nice for decorators - it's clean and unambiguous.

Why do we need to make a special case of decorators that don't use the await hook?

I think this might also make them a lot easier to sell to people who don't really understand the need for asynchronous primitives at all.

jhusain commented 9 years ago

It's definitely an option. I'm not sure how the committee will react to Allowing N modifiers on a function, and exactly what the implications on the parser would be. The most obvious downside would be allowing await in functions where it was not valid.

benjamingr commented 9 years ago

I'm not sure I understand, doing async function foo(){} return a function already, JS has function expressions, and unlike generators where the * is part of the function async prefixes the function.

What would prevent me from doing

async async function foo(){}

In the current async functions proposal today? It compiles just fine with Babel, it's pointless but it's valid syntactically.

bmeck commented 9 years ago

@jhusain the grammar would get to be in a very bad state, you would need to have the modifiers after the parameter list to avoid confusion like dart does function foo() async cached {}

benjamingr commented 9 years ago

@bmeck I agree that this poses a problem and complicates the syntax, I'm not sure if it does so in an ambiguous way - which is why in my original post and examples I used compose(autocurry, cached) and not autocurry cached.

Perhaps someone with more parser experience like @sebmck could shed some light on the complexity (if at all and how much) involved in something like this and why async async works at an engine level.

bmeck commented 9 years ago

@benjamingr right now it works because async is a well known token that is placed in https://github.com/babel/babel/blob/0112c6377914fcb5a1d0287b0bbc5c986d83c6fe/src/babel/transformation/helpers/remap-async-to-generator.js once it becomes a generic identifier things start to get stranger because it requires lookahead.

https://github.com/babel/babel/blob/cf7d6b655e7a6012491f6a8e0133727396fc15a4/src/acorn/src/expression.js#L277

benjamingr commented 9 years ago

@bmeck thanks, that definitely explains it. How would syntax resolution look at this proposal? Does this mean you can only create a compositional function when it's created? (So you can't make a function async from the outside). To be admit, I can't see compelling use cases to make a function "async from the outside" (it also has issues since await would not be well defined if we don't know in advance if it's a compositional function or not)

I posted an issue on the decorators proposal, I think that if decorators are given a hook to await we pretty much get this very proposal only with different syntax. I wonder if it can't be solved in all three places at once.

bmeck commented 9 years ago

@benjamingr as it stands async is not done at runtime, it is done at compile time which is partially reflected by the syntax, decorators proposal is done at runtime. The syntax most likely would want to change for await if we had runtime decoration be possible, however async/await introduce a new keyword inside the scope so they should not be decorated at runtime. Instead use a function wrapper (or the decorator proposal) to wrap them:

let foo = @cached async function() {
}

This is all due to compositional/async functions introducing new syntax in the function. We don't allow function decorators to introduce new syntax and probably should not.

TL;DR - compositional changes the yield keyword to await which makes it unable to be combined fully w/ function decorators.

bmeck commented 9 years ago

@benjamingr on a different note regarding making async into a decorator by itself, this is possible if we used yield as the key word instead of await. I would generally even prefer this on a personal style level.

let foo = @cached @async function* () {
}

is fairly easy to parse / does not require lookahead.

let foo = cached async function() {
}

would require lookahead after lexing async to see if the tokens eventually form a function after the list of identifiers.

benjamingr commented 9 years ago

Yes, I see that now. Wouldn't it be possible to require that decorated functions reserve await as a keyword? It'd mean it's more work to desugar but it also means this whole thing can be done in the userland (I definitely think the language can, and should provide @async or async) with a hook.

This would also have the same advantage as this proposal of allowing us to use userland implementations and prototype awaiting different things (like cancellable promises for example).

That said, in its own async/await is an enormous improvement for the masses in writing async JavaScript, it's a bit of a shame you're stuck with the engine's and language's implementation and idea of what primitives you must use but it's possible to yield observables this way (by a then, or a proposed symbol) and it's still a big improvement over the status quo.

bmeck commented 9 years ago

reserving keywords is pretty cheap for compilers, but I would want to ask people about the goals for language macros (was planned for es8 / es2017) before saying anything about reserving more keywords.