4 ms·
I assume this is exactly what SomeCallMeTim meant. Yes, explicitly awaitable futures/promises are slightly less ergonomic than coroutines, but this is a very re
by unscaled 10y ago
I assume this is exactly what SomeCallMeTim meant.
Yes, explicitly awaitable futures/promises are slightly less ergonomic than coroutines, but this is a very reasonable trade-off.
You give up:
1. Extra cognitive load every time you need to await.
2. You've got to be more careful when designing some generic higher-order functions (you need to decide which color their function arguments should be).
In return you get:
1. Explicit concurrency. No function that you call unexpectedly suspends behind your back. In some cases this explicitness could be actually important (e.g. when using per-thread/process shared memory).
2. Futures/promises/tasks are first-class values. You could pass them along, save them for later, await on all or any of them and so forth.
3. Theoretically speaking, promises can be more memory efficient than coroutines since you don't need to allocate a full stack for each of them - just the state you need.
"What Color Is Your Function" is misleading: there is no strictly superior solution for this tradeoff.
Futures/promises encode concurrency as a monad and like every monad they are viral and once you start propagating them around, they can be a pain.
But the alternative for not encoding some concern of your program as a monad is to let it pass through an implicit side-channel (I/O, error-handling, global state, built-in scheduler) that is harder to control and can't be treated as first-class object in the language.
Go itself chose to go the explicit with error-handling. It doesn't even uses Monads, so it gets the worst of both worlds in my opinion, but the designers' main concern with exceptions which they managed to avert is still valid: with (unchecked) exceptions you never know which function you call is going to raise an error on you somewhere down the line.
So yes, with Go you'll never have with unexpected throws, but you'll have to deal with unexpected goroutine suspension.
In Go all of your functions will be happily colorless with regards to concurrency but will be colored with regards to error-handling.
Again, both choices here are valid, but they are not so brain-dead simple or universal as this article is trying to put them.
- SomeCallMeTim 10y agoVery well put, thanks. Explicit promises have very nice, composable behaviors that I'm exploiting all the time, and that have no equivalent in the Lua coroutine world. And once you have propagated them around, I think the ergonomics are just fine; I've taken some pretty hairy callback logic that no one could fully understand without careful study and turned it into async/await code that pretty much anyone could follow. I think the implicitness of "this could suspend" does frequently cause serious problems; in Lua this shows up as the dreaded "Attempt to yield across a metamethod C call boundary" [1] bug. Talk about cognitive load! If you're calling C from Lua, and that C code then calls back into Lua, and the latter function yields, you get this error. So you have to be very careful about what is allowed to yield -- and since any function in Lua, at any point in the call stack, can yield, it's easy to accidentally shoot yourself in the foot. [1] http://stackoverflow.com/questions/8459459/lua-coroutine-error-tempt-to-yield-across-metamethod-c-call-boundary http://stackoverflow.com/questions/8459459/lua-coroutine-err...
- anonymoushn 10y ago> 2. Futures/promises/tasks are first-class values. You could pass them along, save them for later, await on all or any of them and so forth. Can you explain why this is unachievable other than by tagging your yield-capable call sites?
- unscaled 10y agoYou can acheive some composability (e.g. easily awaiting multiple events) by having first-class values for the coroutines/green-threads themselves. This is what greenlet/gevent does, and it's rather pleasant to use. Try doing something like gevent.joinall() on Go though. https://nathanleclaire.com/blog/2014/02/15/how-to-wait-for-all-goroutines-to-finish-executing-before-continuing/ https://nathanleclaire.com/blog/2014/02/15/how-to-wait-for-a... Not fun. The other problem is that even with gevent, what you get is a first-class _coroutine_, not a first class class _continuation_. You have to actually spawn new coroutines (creating new stacks and going through sometimes unnecessary scheduler hoops) to get a first-class coroutine out of a possibly blocking function. Again, I'm not saying you can't do something like gevent.joinall(gevent.spawn(foo()), gevent.spawn(bar()), but: 1. Many languages don't support this (including Go and Lua, AFAIK). 2. It comes with its own tradeoffs again.
- anonymoushn 10y ago> 1. Many languages don't support this (including Go and Lua, AFAIK). I don't think this is necessarily a property of the primitives. greenlet doesn't support anything like gevent.joinall(gevent.spawn(foo()), gevent.spawn(bar()), but gevent does by adding python code to greenlet. So I'm sure that in the same manner one could do it in Lua using Lua's coroutines. (There might be some concern that we have the wrong primitive: greenlet provides symmetric coroutines and Lua provides asymmetric coroutines. I don't think it matters for this which one we use, but if it does it is simple to implement either in terms of the other.) That's not to say that the situation is ideal! To a great extent it's good if a language comes with a standard way to do this stuff! But it would be silly to say something like "Lua doesn't have inheritance" because it has a DIY inheritance construction kit instead of an "extends" keyword.