-
Notifications
You must be signed in to change notification settings - Fork 2k
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
backcalls: let's add them #2762
Comments
As observed back on #2662, backcall syntax works well with RequireJS: ($, someLib) <- require ["jquery", "someLib"]
codeThatUses $, someLib Another potential use is x <- amb [1,2,3]
y <- amb [4,5,6]
amb() if x + y < 10
x * y |
Also, for any Haskell fans, yes, this is just |
From the previous post I proposed (a, b) <- fn c, ^, d
# use (a, b)
e |
JFYI, it originates from #1032. |
Also, we could always drop the concept of a placeholder and force people to force the callback as the final argument. There's @00Davo's approach from #2662 of defining higher-order functions such as flip = (f) -> (x, y, rest...) -> f y, x, rest...
<- (flip setTimeout) 250
do takeAction And there's also just manual partial application: <- (_) -> setTimeout _, 250
do takeAction |
i’d vote for dropping placeholder +1 it brings more complexity, still |
Using the right variable name you can have a person giving you a hug there:
I'd also vote against having the placeholder syntax; unless that concept of placeholder could be extended and used elsewhere too. In general i'm not much of a fan of this proposal, but i must admit that the use cases @00Davo mentions are quite really 😃 |
Is there any reason to automatically |
I'll throw in my hat in favor of the placeholder syntax. Just reading the code, this: (a, b) <- fn c, ^, d
# ...
e is infinitely more clear than this: compose = (f) -> (c, d, callback) -> f c, callback, d
<- (compose fn) c, d
# ...
e The latter also requires a separate "helper" function for each non-"standard" argument style that needs to be adapted for, harming interoperability with existing code. |
A compose function is unneeded here. I'm in favor of the hug approach |
@Nami-Doc Ah, missed that. Yep, that looks good to me. |
Why not? In one of the use-cases mentioned in this thread, defining AMD modules, returning a value from the backcall would be really important: ($, _) <- define 'thing', ['jquery', 'underscore']
# Very typical use-case for AMD modules: returning a constructor function.
class Thing
foo: -> # ... |
What if intermediate callbacks need to have a value returned? Would backcalls not be usable in this situation? |
Because we can't no-op them if chained (for example).
I'd probably vote +1 tho. |
@Nami-Doc I don't understand. Couldn't you tack a |
but that would no-op the inner backcall, not the one with the loop ;-). |
@Nami-Doc: That's why you would never write it like that. <- epi
for i in [0..2]
<- dem i
an
return or my preferred <- epi
for i in [0..2]
dem i, -> an
return I hate bad code strawmen. You can write bad code in any language, we get it. |
@Nami-Doc Ah, so <- dem i for i in [0..2] translates to for i in [0..2]
<- dem i
# ... and not <- dem (i for in in [0..2])
# ... ? |
Considering |
Thinking about this a bit more, conditionals and loops complicate things quite a bit. Consider: if a
b = <- fn c
d = b
else
d = e()
console.log d b =
if a
d <- fn c
else
d <- fn c
console.log b console.log
for item in array
element <- transform item result = null
console.log
until result?
result <- generate Lots of messy edge cases to handle. |
Why are they complicated in your examples? |
@mintplant: They don't complicate anything at all. The captured continuation is the rest of the block. Maybe you're thinking it's the rest of the function/program? |
Ah, I see what you mean. I suppose I misunderstood the scope of this syntax addition. So, these only help with non-branching code paths, then? |
WTF happened to my name? xD Anyway, between all those messages i got lost. The consensus was that it does make sense to return values from backcalls, wasn't it? |
@mintplant: They work perfectly fine with branching code paths. It is a very simple transformation, you're overcomplicating it. @epidemian: Yeah, it makes sense to auto-return from backcalls. That's why |
Sorry, no idea why I came up with that :p . And yeah, that'd make sense. |
@michaelficarra Right, I get that now. I never said they didn't work with branching code paths, just that they don't provide any special functionality to help account for them, which I had hoped they would, but now see is outside the scope of this change. Sorry for the misunderstanding. |
The "hug-style" As for the higher-order function approaches, this seems problematic: compose = (f) -> (c, d, callback) -> f c, callback, d Because it looks nothing like a standard definition of I think very simple higher-order manipulations, like |
Hugs it is! I will edit the original proposal to omit the callback position indicator. edit: done. Copied below: And for the rare occasion where the continuation is not the final argument of the function, (a, b) <- (_) -> fn c, _, d
...
e compiles to (function(_){ return fn(c, _, d); })(function(a, b){
...;
return e;
}) or, if we detect this case, the simpler: fn(c, function(a, b){
...;
return e;
}, d); |
This would make @tenderlove happy |
Also, what about other types of deferred? Are we expecting the callback to be the last argument in the function?
Hey look at that, a pretty decent reason to use
|
Also, what about parallel? I don't see the need for async anymore, the whole point is to make coffeescript good enough to do it on it's own, right?
There is no In the past my suggestions have been shutdown, probably due to the difficulty that would be in implementing them, so it could end up being that a new compiled js language emerges, inspired by the beauty that is coffeescript, while fulfilling my wildest dreams. |
We can't have
Yes. In earlier iterations support for putting a "placeholder" elsewhere (to relocate the callback) was suggested, but eventually it was decided that simply wrapping the expression with a function would suffice. Look for the "hug operator".
By far the most common pattern for callbacks is to use a single callback with an x = someSyncCall arg, arg
y <- someAsyncCall arg, arg
f x, y Most calls that require more than one callback just don't make sense if you view p = (pr) -> (f) -> pr.then f
backcallCode = ->
x <-p somePromiseCall arg, arg
y <-p someOtherPromiseCall arg
x + y
directPromisesCode = ->
somePromiseCall(arg, arg).then (x) ->
someOtherPromiseCall(arg).then (y) ->
x + y Error is simply propagated past the backcall chain, invoking none of the success-path calls. This essentially corresponds to the way the promise monad's error-handling basis, the Either monad, works.
Actually, no. If we were trying to make CoffeeScript's asynchronous support complex/powerful enough to model all async patterns by itself, we would probably have jumped straight to IcedCoffeeScript. The problem is that nearly all asynchronous patterns other than simple serial will compile to JavaScript code that's a lot less pleasant: Running calls in parallel requires some kind of reference-counter to be declared and tracked, for example, and at the deep end we end up with the monstrosity that is CPS-transformed JavaScript. Backcalls are a simple enough syntactic transformation that the compiled JavaScript is not particularly horrible. All they do is flatten callback pyramids. They do not and will not model all async patterns, because that need is much better served by a library such as caolan/async. Such libraries would be used in conjunction with backcalls: (err, [one, two]) <- async.parallel [oneF, twoF]
codeUsingAsyncResults one and two |
-1 Let's not. Promises or generators are the way out of callback hell, otherwise I (did) would use IcedCoffeeScript. |
@xixixao Absolutely they are, but backcalls a) work quite well with promises in conjunction with a helper like the Generators are definitely a more flexible way to make promise code look synchronous, but they're still not available in every browser, nor do they exist in CoffeeScript yet either. Meanwhile, generators are not going to help you at all with cases like: export = (x) -> module.exports = x
grunt <- export
grunt.loadNpmTasks 'whatever'
grunt.allYourGruntStuffHere withNoIndent Or: $, _ <- define ['jquery', 'underscore']
requireJS.module goesHere withNoIndent Or even: result = do ->
x <- amb [1..10]
y <- amb [1..30]
fail() if x * y < 10
x*2 + y*2 |
Is this still valid? Does somebody investigate/develop them? |
A while ago I prototyped a basic implementation: https://github.com/zhaizhai/coffee-script/tree/backcall In particular, look at https://github.com/zhaizhai/coffee-script/blob/backcall/test/backcall.coffee In my opinion it ended up being weird to not be able to write if statements and for loops normally with "backcalls" inside, so you would really need to compile the appropriate CPS transformations on those constructs to make things feel natural. At that point maybe you should just use IcedCoffeeScript. If you're interested the code linked above should still work, but presumably it's a ways behind the main branch by now. It seems like people are not that interested in this feature anymore. It's an interesting discussion topic and attempts to address a real problem, but nobody (including me) actually tries seriously to get it done 😛 . |
I see, thanks! |
That's really useful feedback on the feature. Thanks for sharing it! |
I don't get how you can't use if and stuff in a backcall ? You're saying the backcall doesn't work like await. We're aware of that, that's not what we're proposing here. EDIT : We're not proposing an alternative to iced's |
I think the thing with using if statements is talking about a situation like the following (using =< for the backcall operator, as discussed above): if is_async get_x_using
x =< get_x_using y
else
x = get_x_using y
.... It's true that this makes things hairy for the compiler. Of course, the whole if/else block above could be refactored into a backcallable function; but zhaizhai is saying that, upon playing with it, that seems artificial (if the programmer does it) or ugly (if the compiler does). I'm not sure what the right answer is, but it's probably worth leaving this one open for a little longer to see if somebody comes up with a good solution for this issue. For instance, I suspect you could make a "hugs"-like idiom for a workaround.... |
But -- as I said -- we don't want |
Looking back on this again it just looks confusing. Won't this be sort of moot when generators and yield become the norm? |
|
I skimmed through the thread but didn't find a convincing example why monadic notation is useful, so I decided to compile my own. Please review: https://docs.google.com/document/d/1pmvd6Gd-Scj06dB6uLunU52eNpMCjAqM9t6HobOJ758/edit# |
Just a thought an alternate syntax for this, maybe this would be easier for people to understand? What if we did "->..." meaning "make a function, but the indentation for it is on the same line", so...
is equivalent to
Also, for the problem with the function parameter in a funny place, it could be solved like this:
is equivalent to
|
I just saw that ES7 will have Should I open another issue for |
Feel free to open another issue that specifically talks about what adding the ES(7) keywords would look like — and how we might compile them. I don't think it's something that's possible without large-scale runtime support, no? |
+1 to @QxQ syntax |
I appologize for the long comment, please forgive me; but; I really think that a backcall operator obfuscates the code, imagine this:
Now, does this traslate into:
or:
with the proposed syntax you do not know whether or not the second function should be a part of the inner-function of the first or if it should run in parallel. I would rather wrap my code in a yield statement like so:
this would resolve into the first option. For the second option you would get:
This results into much cleaner code. Now resolving a promise becomes almost trivial:
Now for the problem of the order of the parameters and the callback being somewhere in the middle, combined with mutiple parameters within the callback:
Here you sacrifice readability for distance to code. |
@Baudin999 Backcalls always desugar into the second of your proposed syntaxes, i.e., a <- thingOne
b <- thingTwo
moreCode
// *always* means
thingOne(function(a) {
thingTwo(function(b) {
moreCode;
});
)}; This is consistent with the behaviour of backcalls in Coco and LiveScript, Haskell's {items, otherStuff} <- async.parallel items: getItems, otherStuff: getOtherStuff
# or with promises and a 'p' helper as discussed earlier
results <-p Q.all [firstThing(), secondThing(), thirdThing()] Allowing for distinct |
FWIW, I'm contemplating switching IcedCoffeeScript over to a generator-based transpilation, which is of course much simpler. Here's a hand-compiled example of how it can work. |
Skimming through this, it seems that the consensus was to not implement this feature. As such, I’m closing this ticket. I think at this point if someone wants to re-propose backcalls, please start a new issue or pull request. Any new proposal should be aimed at the |
Copied proposal from #2662 (comment) below
Taken pretty much from the coffee-of-my-dreams list, the best way to explain my preferred backcall syntax is through example:
The continuation is captured and tacked on as the final arg by default
<- fn a ... b
compiles to
The left side of the arrow specifies the argument list for the continuation. When only one is given, parens can be omitted.
a <- fn b ... c
compiles to
And for the rare occasion where the continuation is not the final argument of the function,
we need to be able to specify where to put it with a marker like thewe can simply use an anonymous function to force the position of the callback.<&>
I use below. I'm open to suggestions for a better markercompiles to
or, if we detect this case, the simpler:
this
must be preserved in the continuation by rewriting references as we currently do in bound functions. I don't think we should worry aboutarguments
rewriting, but that's your call.Here's a real-world example @paulmillr gave in #1942:
and here it is using backcalls:
edit: dropped the callback position indicator, opting for "hugs" style
The text was updated successfully, but these errors were encountered: