They're equivalent except that asynchronous callbacks is what actually happens and you have clear control and visibility on how control flow moves.
They're equivalent except that asynchronous callbacks is what actually happens and you have clear control and visibility on how control flow moves.
I currently work on a project that involves Java code with a promise library, and Unreal Engine C++ code which does not and uses callbacks (and do async JS stuff in my personal projects), and both have to do asynchronous logic. The Unreal code is just so much harder to deal with.
Specific problems the Unreal code has:
- There's no "high level" part of the code that you can look at to see what the logic flow is.
- Many functions are side-effecty, triggering the next part of the sequential logic without it being clear that that's what they're doing. Like the handleFetchAccount() callback kicks off another httpRequest for the next step, but you wouldn't know that it does that just from the name.
I'd admit some of these problems might be mitigatable in a better written codebase though.
Consider this example algorithm, of several async steps,
1. Download a file into memory
2. Email a link to a review web page.
3. Wait for the user to review.
4. Upload the file to a partner.
5. Update a database.
You could implement this as callbacks. Callback from each step leads to the next being triggered. Downside - your business logic is spread across all the callbacks. You could mitigate this somewhat by defining a class with one method for each step, with those methods being defined in the same visual order as the algorithm. Then have each callbacks call a method. (The article shows something different but similar with its Command autoCommand pattern.)
Tricks like this only go so far. Imagine if the reviewer user had a choice of pressing 'approve' or 'reject' on the webserver interface, with the algorithm changing depending on their answer. How do you now represent the business logic so the programmer can follow it?
Such changes are easy in coroutines. Here is the algorithm with that variation in coroutine code,
async def review(review_id, url):
file_content = await download_large_file(url, review_id)
ws_review_id = await create_webserver_review_page(file_content)
await email_the_user(ws_review_id)
result = await get_webserver_user_response(ws_review_id)
if result:
await upload_to_partner(file_content)
else:
await alert_failure(review_id, file_content)
await update_database(review_id, ws_review_id, result)
You state that callback code gives you easy visibility to what actually happens - yes, they do. When you read callback code, it is natural to follow business-logic to system calls. Coroutine tends towards code of layered business-logic and abstraction.Neither stackful nor stackless coroutines work like this practice. The former suspends coroutines by saving and restoring the CPU state (and stack) and the latter compiles down to state machines, as mentioned in the article. Coroutines are functionally not equivalent to callbacks at all.
Stackful coroutines are just a poor man's threads.
I found the article a great example of the kind of crap that passes for programming these days.
But that isn't the kind of lesson that you want to be cramming into the middle of the competition season.