5 ms·
I would strongly prefer to not have to change every call site of a function (and the functions containing the call sites, and so on) when adding 1 yield to it a
by anonymoushn 6y ago
I would strongly prefer to not have to change every call site of a function (and the functions containing the call sites, and so on) when adding 1 yield to it and to be able to make higher order functions that work on functions that yield and functions that do not yield.
- maccard 6y agoAgreed. The "what colour is your function" [0] article comes to mind. Even with that, this is an impressive project. [0] http://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/ http://journal.stuffwithstuff.com/2015/02/01/what-color-is-y...
- fleabitdev 6y agoGenuinely interesting suggestion! I used Lua coroutines, Python generators and Ruby fibers as my prior art; in all three cases, they're resumed by invoking a method on the coroutine object. A lambda function which resumes a coroutine can simply be written as: (fn0 (coro-run the-coro)) This is more ugly than passing in the coroutine directly, but it's also more explicit. I'm concerned that if the "resume coroutine" operation looks like a normal function call, the control flow of coroutines might become too difficult to follow. To the best of my knowledge, the only way to get rid of `yield-from` would be to switch from stackless to stackful coroutines. GameLisp used to have stackful coroutines, but they added a large complexity burden to the virtual machine, and they were so expensive that I could never bring myself to use them for entity scripting. My instinct is that other game developers would feel the same way. I'll need to give this some more thought.
- anonymoushn 6y agoLua coroutines have exactly the thing I asked for though. As an example, if I have some enemy in a shmup that moves (over the course of many frames), fires some bullets in an arc centered on the player (within one frame), and moves again, I can write something like this: function ExampleEnemy:behavior() while true do self:move() self:fire_arc() end end function ExampleEnemy:fire_arc() local theta = self:aim() for i=-5,5 do self:fire({speed=8, direction=theta+i*10*degrees}) end end If I later want the arc to have some delay between the bullets, and I insert a call to a function that yields for a certain number of frames in the loop in fire_arc, the code continues to work. If I do this same change in Python, the result is that the call to fire_arc in behavior begins to return an iterator or something and silently fails to actually fire any bullets! Lua's stackful coroutines are sufficiently cheap that it's reasonable to have tens of thousands of entity behaviors implemented as coroutines running at a time.
- fleabitdev 6y agoVery good point! I'm caught between a rock and a hard place... Apart from their other downsides, stackful coroutines can make asynchronous code less readable. For example, it's not obvious that your self:move() call is split across several frames. Implicit coroutine construction has the silent failure mode which you describe. Explicit coroutine construction, (coro f), would make `yield-from` even more noisy than it already is! I wonder whether a naming convention would solve the problem? If all coroutine functions and methods have names like fire-arc* and move* , then your synchronous call to fire-arc* would stick out like a sore thumb. This would also force the programmer to visit every callsite after changing a synchronous function into an asynchronous one, or vice versa. It wouldn't exactly be convenient, but I think I value explicitness more highly than convenience in this case.
- john-shaffer 6y ago> I wonder whether a naming convention would solve the problem? If all coroutine functions and methods have names like fire-arc* and move* , then your synchronous call to fire-arc* would stick out like a sore thumb. That'd be fine as long it's simple to write a wrapper of a different color. If I change fire-arc to fire-arcSTAR, but I can do a quick (def fire-arc (to-sync fire-arcSTAR)) then I don't have to visit call-sites. If it's not possible to make a wrapper, then that would be a major pain point. Edit: Can't get HN to print the asterisks.
- fleabitdev 6y agoBut the * suffix marks functions which might stagger their execution over multiple frames. If a function calls fire-arc* , then that function should also have the * suffix; otherwise, the suffix would be meaningless. You're asking for the ability to refactor a synchronous function into an asynchronous function without needing to review the function's callsites. By definition, this would come with a high price: when writing a step handler, you'd need to assume that any function call could be refactored so that it doesn't return until several frames in the future. Even for a Lisp, that seems chaotic!