Tame.JS: Flow-control by the makers of OkCupid.com
tamejs.org
tamejs.org
How long 'till the port to CoffeeScript syntax?
https://github.com/jashkenas/coffee-script/issues/241
https://github.com/jashkenas/coffee-script/issues/287
https://github.com/jashkenas/coffee-script/issues/350
Edit:
Things look a little less promising after running a simple test. This input JavaScript:
while (i--) {
twait {
fs.readFile("one");
fs.readFile("two");
}
}
Gets compiled into this resulting "tamed" JavaScript: var tame = require('tamejs').runtime;
var __tame_fn_0 = function (__tame_k) {
var __tame_k_implicit = {};
var __tame_fn_1 = function (__tame_k) {
if (i --) {
var __tame_fn_2 = function (__tame_k) {
var __tame_ev = new tame.Event (__tame_k);
var __tame_fn_3 = function (__tame_k) {
fs .readFile ( "one" ) ;
fs .readFile ( "two" ) ;
tame.callChain([__tame_k]);
};
__tame_fn_3(tame.end);
__tame_ev.trigger();
};
tame.callChain([__tame_fn_2, __tame_fn_1, __tame_k]);
} else {
tame.callChain([__tame_k]);
}
};
__tame_k_implicit.k_break = __tame_k;
__tame_k_implicit.k_continue = function() { __tame_fn_1(__tame_k); };
tame.callChain([__tame_fn_1, __tame_k]);
};
__tame_fn_0 (tame.end);
... not so nice to work with or debug. The general conclusion of that series of tickets was that the code generation required to make this CPS transformation work with all edge cases is a bit too hairy to be worth it on balance. Depending on how much sequential async you're doing, YMMV.Edit:
That compiled script looks a wee scary. I need to be able to fully dive into a debugger with clarity and can't imagine if there were tens or hundreds of lines of this.
I.e., it has well-defined semantics and "only" involves JS rewriting, which is CS's forte.
The stopping issue was twofold: Coffeescript cares about the readability of both the compiler input and output and… Coffeescript cares about not inserting dynamic lookups and calls into the program which can add undefined performance impact.
Perhaps a fork of coffeescript that is targeted for writers of async NodeJS code would be able to safely trade those off. I think jashkenas believes that Javascript will benefit from many little languages springing up to solve specific problems.
In tame/C++, debugging either compiler or runtime errors is straight-ahead, as the developer doesn't need to examine the mangled code. Now if only JavaScript had the equivalent of cpp's #line directive....
From my understanding of the prior work the issue with adding defer or <- to CS was that it required too much overhead to get right in all cases. Does TameJS' approach improve that overhead in any way, or is this essentially the same work that's already been explored for CS, broken out into a dedicated compiler?
huntMen =(buffy)->
soulmates =(buffy, cb, mates=[])->
getMatches buffy, 10, (userids)->
for u in userids
do (u)->
getThumbnail u, (thumb)->
isPicAVampire thumb, (is_vamp)->
unless is_vamp
getPersonality u, (personality)->
getLastTalked u, match, (last_talked)->
soulmates.push userid: u, thumb, personality, last_talked
if soulmates.length >= 10
cb(mates)
else soulmates(buffy, cb, mates)
else soulmates(buffy, cb, mates)
soulmates buffy, (soulmates)->
#Do whatever you need to with soulmates
Obviously it's still more of a hassle to deal with than a more featurey async library, but there isn't quite the panic of trying to do the same thing in JS.Edit: For comparison, the Tame.JS style transliterates to ~15 lines of CS— but presumably that would compile to dozens more lines of JavaScript.
In exchange, it is lying to you about what the program is actually doing, as opposed to the callback syntax which obscures nothing. So yeah, not surprised if you don't get too much traction with examples like that.
...
isPicAVampire thumb, (is_vamp)->
unless is_vamp
mate = userid: u, thumb: thumb
finish =->
return unless mate.personality? and mate.last_talked?
soulmates.push mate
if soulmates.length >= 10
cb(mates)
else soulmates(buffy, cb, mates)
getPersonality u, (p)->finish mate.personality = p
getLastTalked u, match, (lt)->finish mate.last_talked = lt
Anyway, I'll grant you that code is pretty harsh. It's much nicer with syntax highlighting and wide tabs, but still, fair cop. The flip side is that that code compiles into JavaScript that does exactly what it says.I'm really fascinated with this thought that adding sugar/coroutines is "lying". Everything is based on abstractions; this is just another one. This is not a different "lie" than any other abstraction (assembly, C, OS, JavaScript).
Node comes with single-threaded, callback-based async that works quite well, but it's a bit of a hassle to use for complex stuff.
Nothing wrong with that— abstraction time! The async module, for example, comes with a few different callback-based flow-control patterns. The Buffy example would look something like this:
huntMen =(buffy)->
soulmates = []
getSoulmate =(callback)->
mate = {}
async.series [
(cb)->getThumbnail u, (thumb)->cb null, mate.thumb = thumb
(cb)->isPicAVampire thumb, (is_vamp)->cb('vampire' if is_vamp)
(loaded)->async.parallel [
(cb)->getPersonality u, (p)->cb null, mate.personality = p
(cb)->getLastTalked u, match, (l)->cb null, mate.last_talked = l
loaded
]
], (err)->
soulmates.push mate unless err
callback()
async.whilst (->soulmates.length < 10), getSoulmate, ->
#Do whatever with soulmates
(I don't actually use async much, so forgive me if there's an error there.)Async's abstractions are what I would call "honest". It's still using callbacks, it's obvious what the relationship between them is. The meanings of 'series' and 'parallel' are clear; I could write them out myself, it'd just take longer. Nothing about what this code does is being obscured by the abstraction, just made prettier. I know (or can easily work out) exactly what will be executed.
Tame is abstracting the same single-threaded, callback-based async, but it's trying to make it look like it's using threads. I know in theory it must be turning my code into callbacks which are being passed around, but it's deliberately trying to make that unclear. The result is that I have a somewhat worse understanding of what my program actually does.
To be clear, I don't have a huge beef; like you say, abstractions are necessary, and Tame seems fine to me. It's just my personal preference for more honest abstractions over more dishonest ones (no doubt heavily influenced by the fact that I actually love callback-based async, which I think puts me in a small minority.)
I've run into the problem of endless chained callbacks in C, where it's much worse due to the lack of nested functions, let alone closures or garbage collection.[1] I ended up using the switch block "coroutine" hack [2] for the worst cases, along with dynamically allocated "context" structs to hold "local" variables. A proper macro system would have helped transform blocking code into CPS form. I tried to integrate a SC [3] pass into our build, which could have done it, but ran into all sorts of practical/yak shaving problems, so I ended up with the C preprocessor macro/switch solution for now. In user space, explicit stack-switching with something like swapcontext() is probably preferable, if you can get away with it, but in the kernel this is rather problematic.
[0] https://github.com/pmj/MultiAsync-js
The reason I wrote my own was because I originally needed it in Rhino, the JVM-based JS implementation, and I couldn't find anything similar that worked there.
[1] Yes, there are garbage collectors that work with C, but to my knowledge, none of them can be used in kernel modules. In any case, the other 2 issues are worse and aren't solveable within the language via libraries.
[2] http://www.linuxhowtos.org/C_C++/coroutines.htm
[3] http://super.para.media.kyoto-u.ac.jp/~tasuku/sc/index-e.htm...
Agreed that twait conflates concurrency with blocking/callbacks, but it my experience, it's a natural and useful combination.
doOneThing(yield Callback("key1"))
andAnother(yield Callback("key2"))
res1 = yield Wait("key1")
res2 = yield Wait("key2")http://blogs.msdn.com/b/ericlippert/archive/tags/async/
Cool to see growing interest for this at the language level.
Tame.js looks nice in that it's very simple to learn but ~300 lines of plain old JavaScript[1] can give you a general purpose deferred/promise library with more flexibility should you need to do something other than wait on N async operations and then use the results.
[1] https://github.com/heavylifters/deferred-js/blob/master/lib/...
It's not that it doesn't work and you can't do it, it's that the code becomes a mess and their example gets at that. It probably doesn't seem like a big deal, but when you constantly have to pass around callbacks and chain requests together you get this feeling the code could look a lot cleaner than it is. You want asynchronous behavior but with synchronous syntax.
This isn't possible without adding something, and, having seen a lot of the solutions out there, it's nice to see someone take a stab at it by changing the language. The cost is huge (preprocessing, etc.), but, speaking from experience, the simplicity of the code you write might make the change worth it. You get the feeling in 5-10 years this will be a solved problem, but I'm not sure any of the solutions out there yet will be the accepted solution.
mkevent is like creating a Deferred and twait is like creating a DeferredList and calling when/then on it with the remainder of the code after the twait block used as the callback. Conceptually this is a very specific and concrete subset of functionality that promises give you.
(By "add them to JS" I mean some syntactic sugar, not a library)
https://developer.mozilla.org/en/New_in_JavaScript_1.7#Gener...
Unfortunately, V8 doesn't support generators, as far as I can tell.
yield;
Will block execution until the generator is resumed and return control flow to the caller. I haven't actually tried it, but it should be pretty straightforward to wrap a generator function in such a way that it's driven (resumed) by callbacks from async I/O calls.Use a proper control-flow ( not "flow-control" ) library like https://github.com/caolan/async.
Furthermore, why would you write an entire custom js parser for this? Why not use some of the many many pre-existing ones that are much more stable, more developed, and well supported.
How on earth is readable, clean looking code "the wrong solution to the problem"? I would argue that it's almost always the right solution to a problem.
twait{} will block and stop my nodeJS process from doing anything else.
It would be more useful if I could give twait{} a callback to fire when all its async events completed. Then my nodeJS process could do other stuff while waiting for a twait{} bundle to finish.
If so, then it is blocking. Otherwise, how do you manage to let other code be executed, except the code you don't want to be executed until all functions in the twait block have returned?
The would need to be a callback attached to the twait block, but there isn't. So it's blocking.
Because that is it's purpose, to block further executing until all data from the non-blocking functions have returned.
http://groups.google.com/group/nodejs/browse_thread/thread/3...
In another word, write in C, and debug in assembly...