-
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
Asynchronous coffeescript made easy, part III #350
Comments
Here's a direct link to the test_deferred.coffee suite, for folks who'd like to take a peek: http://github.com/gfxmonk/coffee-script/blob/deferred/test/test_deferred.coffee |
Exciting, I hope to try this soon, great work! |
I couldn't get this branch to compile, http://gist.github.com/387224. Any ideas? I am sick of nesting code and using 3rd party libs like step and flow for this so I am anxious to try this. |
@mrjjwright: might have been a bad merge, I'll check it out tonight and let you know. |
done! (just pushed). Not entirely sure what the problem was, but it seems to be sorted now. Apologies for that. |
It compiles now, thanks. |
I tried it on a place today where I was planning to use flow.js. It worked fine. I liked it because I didn't need a library to pull this off. I am always worried that there is a hidden bug in flow.js or step.js. There could of course be bugs in the defer code as well but I am assured that all the code is right there to inspect if it is wrong, and it clear what it is doing. Both flow.js and step.js use underlying state machines that are harder to debug. The defer keyword definitely illustrates at least theoretically the beauty of CoffeeScript to provide a language solution to an awkward JS usage problem. A developer would otherwise use a library but I think directly generated code is a lot easier to inspect and trust than libraries. I don't understand why developers are reluctant to try CoffeeScript but will gladly pop in any third party library that is barely documented. I don't want to get too addicted to this yet but I can see how I could easily do so. +1 |
mrjjwright: for the sake of discussion, it would be great if you could paste in a bit of one of your real-world conversions to the "defer" syntax. I think a lot of the discussion that we'll have will be better informed by "before" and "after" examples with real code... |
Ok I am going to try this on what is surely a great test case, a SQLite table migration where you dump all the results of one table to another and then re-import them. This involves about 15 levels of nesting as well as a nested function definition in a very deep defer. I have this working in flow.js and tried migrating it to defer but running into a compilation issue that I have pasted in the comments below the gist (I could be doing something really naive but couldn't find it). http://gist.github.com/388519 I will probably need your help gfxmonk to get this compiling. |
If this were in coffeescript, I'd be using it now. Even with the all the virtues of asynchronous programming there are still many of things that need to be done serially. This makes clear, readable code out of it. +1 |
gfxmonk helped me get it to compile and it worked like a charm. http://gist.github.com/388519 A few things to summarize from my experience.
jashkenas I am wanting this, it's going to be hard to not lobby you. But this is my first day with this and I haven't heard or read all the naysayers yet. |
I made a suggestion about syntax in an old thread - I'll just repeat it here ... [err, data] = defer get "/", {} my_async(x, y) [err, data] = defer more a, b setTimeout(defer, 100) final() use a loong arrow [err, data] = get "/", {}, --> my_async(x, y) [err, data] = more a, b, --> setTimeout -->, 100 final() I think it neatly encapsulates the idea of 'look to the next line' and also that it's a bit like a function. |
mrjjwright - you might be interested reading the old threads for discussions we had around naming. async has been suggested, but I don't really feel it explains what the keyword does. "call-with-current-continuation" is the best name for it - that comes from smalltalk, but even there it was contracted to call_cc in actual code (which is almost meaningless) I've been discussing this with a friend, and a couple things should also be pointed out as shortcomings:
weepy Being a python guy, I typically prefer meningful words over punctuation. Plus, I think your suggestion makes it too easy to accidentally start a function (by forgetting a "-"). But if we move to having the "defer" keyword replacing positional arguments (rather than prefixing function calls) then defer wouldn't necessarily be a great choice, so suggestions are welcomed. |
oh, and:
Most people dealing with javascript callbacks would already understand continuation-passing-style, and hopefully most have longed for or thought about a way to make that automatic. So hopefully this feature should seem like a fairly natural transformation to most javascript programmers that would encounter it. |
by way of further example, here's an actual javascript file I wrote using lawnchair (a heavily async datastore). I've ported it to both coffe and coffee+defer (reasonably idiomatically, aside from some unimportant string concatenation): http://gist.github.com/389375 |
Hey gfxmonk, Ok, defer is fine as a keyword. I defer. Looping issues, no big deal either, I am always really careful about async calls in a loop. (Btw, ever seen this: http://glathoud.easypagez.com/publications/tailopt-js/tailopt-js.xhtml. Any value here?) Will you be merging all major bug fixes into deferred from the main line for a while so I can use this branch? I am kind of hooked. If not, I will go back to my old crack async dealer, flow.js. |
How about wrapping setTimeout for numerical values? E.g. defer 50 # do something in 50ms You might counter that it wouldn't work for variables that are numeric, but 99% of the time setTimeout's are used with a hard coded numeric value. |
weepy: I think that's more of a library issue - special casing in the parser to deal with deferred numbers instead of deferred calls seems a bit of a strange way to handle it. You could do this with a simple library function:
then just use it as:
mrjjwright: I plan to keep the deferred branch up to date (maybe at a lag of a week or so, because merging is not exactly my top priority). However I can't promise that the syntax won't change (that is, after all, part of the point of this issue / discussion). |
I'm so giddy with excitement over the possibilities for readable nodejs code (as an example) |
Okay, I've just pushed a fairly significant change, which cleans up and clarifies the tricky bits of the original attempt. I now believe my branch is in a good state to consider merging into the master. Pity that jeremy has just disappeared for a couple weeks, but at least I shouldn't have to do too many more big merges while he's gone (they are not much fun). If you want to see the differences, you can take a peek here: It's a big diff, but it's also the first change of its kind. Most of the CS compiler translates one thing into a more primitive version of it (i.e javascript). The deferred machinery, on the other hand, does a lot of rewriting nodes into a manner that will work when calls are "resumed" after an asynchronous call. The first part of the machinery is basically to pull out deferred calls to the beginning of an expression. That is, if a complex expression contains a deferred call then that call is executed first, and the rest of the expression gets evaluated inside the callback provided to the function. This makes all deferred results available at the time when they are needed - because you can't just pause execution in the middle of an addition operation, for example. The second (and quite unsightly) part of the machinery is the rewriting. Just as you can't pause execution in the middle of an addition operation, you also can't fire off a call from within an if block and expect your control-flow to still work when your callback is called. In fact, the callback would have to include the rest of the if branch, as well as any operations that sit after the end of the if block in the original code. So that's where make_control_flow_async comes in. This method is implemented on all nodes. By default it just propagates the call to its children. But in the case of nodes that affect control flow (IfNode, WhileNode, ForNode), it does something pretty invasive. Basically, there's a way to transform each of these nodes into a "flattened" version which manages control-flow by explicit continuations. This method (for each of those nodes) generates the CoffeeScript node objects after that transformation is applied. Because they build up a reasonably different source tree, I've introduced a couple of builder objects (down the bottom of nodes.coffee). I can move them into another file if people feel they clutter the already-huge nodes.coffee file. These builders have named methods for common rewriting operations that make it more obvious what transformation is happening, and why. I'm happy to explain specific parts of the implementation, if people are interested. |
I just came across this; it's very exciting. I'd like to share my vote for positional argument syntax:
The Obviously, this could be a problem since |
mckeed: continue could be confusing because of the existing use (even if it's not ambiguous to the compiler). jashkenas: any thoughts on the possibility of merging this in sometime? You haven't said a word on this since you came back, not sure if you're busy elsewhere... |
I was wondering about an alternative syntax using the err, data: mongo.find {user: 'nicky'}, <- throw err if err process data So the idea is that the arrow points the other way indicate that the tree branch is being unwound, but keeps parity with the normal CoffeeScript. setTimeout <-, 100 run_delayed_code() I think it works nicely because it still mostly looks like normal coffeescript (I found defer to be a bit ugly) |
Hey gfxmonk. I've been trying to knock out most the smaller tickets before tackling the big ones, like There are a couple things you could do to facilitate this...
|
I use |
@weepy: I think it works nicely because it still mostly looks like normal coffeescript (I found defer to be a bit ugly) As for "=>", I'm not sure what it would mean for a deferred function to be bound - that's about function definition, not a function call... @jashkenas: geez, that's a lot of changes. I get distracted writing android apps and all the underscores die ;)
|
ah - after seeing your examples, i can see that my comment about => isn't really appropriate.
|
gfxmonk: Can you explain how |
weepy: yep, that is correct. The exact JS output is not exactly beautiful, but I've now added it to the gist for illustration's sake ( http://gist.github.com/445525 ) jashkenas; regarding return. With defer, you're still writing your functions in a callback style. That is, you take a callback, and you call it with one or more arguments instead of returning a value. The defer machinery makes this look nicer on the call side, so that you don't have to pass an actual function as the callback, it is constructed for you. Keeping that in mind, there's no difference in return semantics to asynchronous coffee-script. If you I'm happy to report that continue and break are indeed problematic, but as far as I know, they are handled properly in all cases :) |
gfxmonk: looks like a couple of things need to be cleaned up in the generated code. To quote:
No need to double-return in either of those places, is there? Also, for the common-case defer, it would be great to write this:
As this, without the variable juggling:
|
gfxmonk: Let me first say that the But I will say that my example was the final api of a library, just as defer would be a final feature of coffeescript. I should have thought it out a bit more and I'd write what I meant, but I see karl has beaten me to the punch. Three things:
becomes this coffeescript:
There's nothing in there that is not explicitly defined in the ECMAscript spec. Now while the ECMAscript spec does talk about callbacks (
A javascript (recursive-functional) program with callbacks runs like so:
Now with
This is a cognitive problem. When I write async code, I assume that sometime in the future the code will be executed, so often I'll go about other business while I wait for it to execute. But what |
thejefflarson:: I'm convinced that a library is destined to be too noisy and awkward. The above example of
The fact that a feature has been implemented and shown to be immensely useful in other programming languages is no guarantee that it's a good idea, but it's hardly irrelevant either. As an aside, I don't love javascript in the least - and if everyone loved javascript, there would be little point in coffeescript...
functions are a primitive. CS uses primitives to turn a for loop into a map / filter operation. The problem of deferred code appearing to be sync is only true if you ignore the |
I really love the idea of My syntactic proposal is to change the assignment operator instead of the with
translates to:
Whats nice is that (Sorry if you already discussed something like this, I'm jumping into this discussion for the first time and there's alot of backlog ;) |
Plus one to Josh's suggestion.
It visually signifies the inverted call order of the deferred function, and makes it obvious that normal assignment isn't happening. In fact, we could lose the destructuring array assignment, and just do this:
It doesn't solve our |
Another +1 for the
|
Had an observation about some simple CPS statements I've seen this pattern frequently to curry on arguments to an async call.
You could write something that accomplishes the same thing with the new syntax.
The compiled code isn't quite the same because of the extra function wrapper. I'm not sure how you'd want to write it to take advantage of the implicit continuation argument. Maybe we could optimize away these identity functions? More involved example
I really like the idea of making |
I was a fan of <- for currying, but if it's free now then I think it fits well with defer (especially since continuation is a monad in haskell, so we can all pretend to be using haskell ;)) josh: there's certainly some amount of work that can be done towards optimisation. Personally, I'm not that interested in it (I think you should probably just use a JS optimiser), but it's certainly possible. Although without a clear separation between the AST and code generation, the optimisations are likely to make for ever-more-confusing compiler code. But yes, we can hopefully tackle the most obvious of inefficiencies at least. |
hmm, some problems just occurred to me with "<-". It's nice for an assignment, like so:
however when not used in an assignment, it would look pretty odd. e.g:
whereas
is still pretty readable. And also, "<-" would cause havok with implied calls (the ones without parens). The following code:
would actually parse (naively) as:
but foo is likely to be undefined, and not at all callable. I think the easiest way to fix this would be to restrict "<-" to not be an expression (make it only valid as a statement), but I'm pretty sure that's not what we want at all :/ or you could require the assignment still occur:
but that's super easy to forget, and doesn't look like haskell any more ;) |
-1 for arrow. Making a simple, often used feature (like defining a function) extremely short is wonderful. It's why I use coffeescript. But making every advanced language feature into punctation is bad. |
I've been watching on the sidelines (and don't even use CoffeeScript yet) so take this with a grain of salt, but based on using Python generators, I'd like to see #1 with some special syntax:
The * stands for the compiler-generated callback parameter that will be called instead of returning. (Justification: it's concise, obvious that there's an extra parameter, obvious that it's a special parameter, and you don't have to scan the function body for 'defer'. The naive user will likely realize that there's something funny going on and read the CoffeeScript manual before calling this function.) For consistency, it seems like we should do the same thing for the special function calls that defer makes:
I also think 'defer' is a somewhat unfortunate name since it doesn't sound like a return statement. (It also does something entirely different in Go.) If we can't have 'yield', maybe 'pause' would get the point across?
'pause' implies both that the function will stop running and resume later, and I think that's the most important thing to get across about an async function call. (It also becomes obvious why pausing within a loop might not be good for performance. Since the generated code isn't that good yet, it might be better to disallow that in the initial implementation.) Passing along an error callback then becomes straightforward if we allow the '*' to appear at any position:
|
Also, on the client side, perhaps 'pause' with no argument could expand to:
|
I'm sadly going to have to come out against the incorporation of defer into the language. Not that I'd not want to use it, but it would legitimately decrease the main argument that I'm using to push CS in my workplace: it's javascript with some great sugar. The semantics are 1-1 and the code generation is intuitive. It's has an hour long learning curve. Up to this point the new syntax has been both intuitive and welcome. However, if we introduce this new defer mechanism, the learning curve for any body of code using it jumps up by an order of magnitude compared to what it is now, since it's a programming construct that would be hard to digest in any language. For this reason alone I feel like it will stand out as a feature that doesn't belong with the others, and would be a inflection point in the overall barrier to entry of CS for Javascript programmers. |
Hi all, So I've spent a while on this, and obviously I'd love to see it merged in to coffeescript core. But it's looking like that won't happen, at least not by me. There are a few reasons:
So it really seems that perhaps defer isn't as good a fit for coffeescript as I had hoped. I'd love for someone to take over and prove me wrong, but I've reached the point of thinking it's not worth trying to squeeze Although I no longer have a vested interest in seeing defer adopted (since I have no plans to write anything big in javascript any more), I'll certainly be taking a closer look at oni and stratified javascript - perhaps in the future stratified JS might be an optional compile target for coffeescript? Anyways, if anyone wants to take over |
That is very sad. A language that does not add new constructs to help with async programming is 'missing the point' of what a compile-to-javascript language should address imo. When I saw coffeescript I thought, "That's kind of neat, but doesn't address the problem that I wish it did." When I saw defer, I was excited. |
To me, the way something like this could come into fruition would be a CoffeeScript fork that takes the project in a different direction with different goals. There's been more than a few things Jeremy has avoided doing with good reason (for example, anything involving compilation into eval), but that's not to say there isn't room for a Javascript-compiled language that breaks these rules to put more power in the hands of the compiler. Defer would fit nicely into such a project, as would other enhancements that introduce fundamentally new concepts on top of Javascript. These are large tradeoffs of course, but I see it as a worthy project once there are more than a few of these types of features that would greatly increase programmer productivity. |
It's not sad at all -- Case in point, Stratified JS: A virtuoso performance of JavaScript compilation, but look at what it compiles into. Taking the Stratified JS version of our
It compiles into this JavaScript:
I don't think we want to take CoffeeScript down that path. Open the Pandora's box of injecting special functions into the runtime, and ... suddenly you have to worry about being orders of magnitude slower than normal JS. So, pour one on the ground for |
To bad, I would have really loved if some kind of language feature like defer would help me solve/save daily problems/loc. |
An exact analogue of |
I'm wondering if try/catch/finally could be used to achieve the readble callback-less sync flow attempted here - as in
I'm assuming a bunch of stuff here but any ideas? |
Nope, finally does not apply to asynchronous events, which is why asynchronous exception handling is so painfully difficult. This section of my post explains async programming as it relates to control flow more thoroughly: http://gfxmonk.net/2010/07/04/defer-taming-asynchronous-javascript-with-coffeescript.html#callbacks |
I find myself wishing CS had defer everyday... it's could be such a valuable paradigm, and simplify the way async code is written. Given that, I have a comment and question... Jeremy: I'm not sure what the issue is with the size and/or speed of the generated code.
Given that, I do understand the need to keep CS's learning curve low. Being able to learn it in one hour is one of it's (many) great strengths. Unfortunately, I haven't had a chance to look at much of CS's internals, but was wondering if there are enough hooks to implement defer as a compiler plugin. And... whether the compiler architecture is stable enough (meaning it's not gonna change much) to make it worthwhile to create such a plugin now. |
Is there a .js that I can include in my pages to just use the latest coffeescript-fork-with-defer (whichever fork that might be)? |
@yang This was not included after all, after long discussions. Coffee-script is staying close to js core features... |
Right, that's why I'm looking past coffeescript and asking about forks. |
Check out Kaffeine Tapped on my fone On 21 Jul 2011, at 17:27, yang
|
@yang: my fork is the most up to date that I know of for defer specifically (https://github.com/gfxmonk/coffee-script/tree/deferred), but it hasn't been touched in over a year (and I have no plans to do so), so Kaffeine (or tame, or stratified JS) is probably a better bet. |
You can also use coffeescript and then tamejs: https://github.com/maxtaco/tamejs |
The main drawback (IMHO) with using tamejs, as was one of the main reasons it didn't get into coffeescript, is debugging ability. But putting that aside, it works really well. I created a gist with a very basic tamejs example written in coffeescript, compiled to tamejs: It does get pretty mangled. 3 lines of coffee script -> 7 lines in "tamejs" javascript -> 44 lines in "expanded" javascript |
previous issues:
http://github.com/jashkenas/coffee-script/issuesearch?state=closed&q=narrative#issue/241
http://github.com/jashkenas/coffee-script/issuesearch?state=closed&q=defer#issue/287
So, it's back!
For those who didn't read or can't remember the previous discussions, I have been working on adding a "defer" semantic into coffee-script. This is aimed at making asynchronous coffee-script less painful, by providing a continuation-like callback object for the current function (at compile-time).
For example:
Would be transformed into the following javascript (or its equivalent):
Special care has been taken such that the following use-cases work as expected:
[err, result]: defer some_call()
)These are non-trivial, so there may be bugs if you do something utterly weird - let me know! ;)
So, please do check out my deferred branch (http://github.com/gfxmonk/coffee-script/tree/deferred) and let me know what you think. Is it a good idea? Should it be (eventually, after some more cleanup) merged into master? Are there any glaring omissions or bugs? Be sure to look at test/test_deferred.coffee, it has over 30 different tests ensuring that as many language features as I could think of work when defers appear in various locations.
(note that the tests in this branch rely on my coffee testing project, coffee-spec (http://github.com/gfxmonk/coffee-spec). I couldn't have managed this many complex tests without it, and it's hopefully coming to coffee-script itself officially sometime soon)
Stuff not yet done
defer
in place of a positional argument - it shouldn't be too hard though.So... thoughts?
The text was updated successfully, but these errors were encountered: