Hacker News new | ask | show | jobs
by LoganDark 826 days ago
Those are objects. It should be possible to nest tasks without introducing object-oriented abstractions. If that isn't possible with this library, then I agree with the original commenter that it's kind of a missing feature.
2 comments

Promise/future/channels are implemented as objects in OO languages. But there are non-OO languages that have them (futures/promises, for sure), and they aren’t objects there.
That's true, Rust has futures and those aren't objects, they're just state machines. However, "awaiting" a future causes it to execute nested inside your own, not as a new task, so it doesn't act as a traditional promise/future. I'm pretty sure GGP was referring to the OOP concept.

Promises in, say, JavaScript, represent the eventual outcome of an asynchronous computation. When you call an asynchronous function, it starts immediately and you get a Promise for it. Asynchronous callers can then await this Promise to give the impression that they're performing a sub-computation, but technically they are just waiting on a new separate task. Futures in, say, Kotlin, are the same thing, and channels are how both of these are typically implemented.

Those kinds of Promises require some bookkeeping that's not free. While it's helpful to be able to just pass around "the eventual outcome of a specific task", that's not actually needed for asynchronous functions to be able to return values. You just need a way for the function to execute nested within another without necessarily requiring a new task.

I've taken a look at OP's library, and it seems to be lower-level than yielding. That means either one of these paradigms can probably be built on top of it. I take back my statement that it's a missing feature, I thought this library was meant to do more than it does.

> That's true, Rust has futures and those aren't objects, they're just state machines. However, "awaiting" a future causes it to execute nested inside your own, not as a new task, so it doesn't act as a traditional promise/future.

That sounds like a lazy future rather than a concurrent one, but both are traditional futures.

> I'm pretty sure GGP was referring to the OOP concept.

Its not really an “OOP concept”. AFAICT, concurrent promises/futures or equivalent constructs mostly appeared in (often relatively obscure) functional-ish and logic languages before the mid-00s, when they started getting implemented in industrially popular, mostly OOP, languages.

> Its not really an “OOP concept”.

It sort of is, because what you're doing is encapsulating the concept of "a value that may arrive in the future" with an object. Both Promises (in JS) and Futures (in Kotlin) work this way.

What I'm saying is that you don't necessarily need to encapsulate that. Rust doesn't: Futures may be called "futures", but they're actually more like "suspendable computations" (not to be confused with generators, which have their own proposal, or coroutines, which also have their own proposal). You can await one as part of another suspendable computation, but you're not holding an object that represents a concept (as in a Promise), you're holding the computation itself. By awaiting it, it becomes a part of you.

The only place a Promise/Future is necessary is when you're not using the computation directly: when you chuck a function off to a task scheduler in order for it to be executed for you. Then, in order to get anything back, you need a handle to that eventual return value. There's your Promise/Future. But if you simply don't chuck it off in the first place, and integrate it into your own suspendable computation instead, you don't need any sort of Promise/Future.

I should note that having taken a look at this library myself, it seems like both paradigms should be possible to implement on top.

I don't see how the abstraction of a future or promise is any more inherently object oriented than than the abstraction of a first class function. They each involve a handle for a computation with particular features, and they each are unnecessary when directly embedding the computation rather than passing it around, but a first class value representing a computation is not a fundamentally object-oriented concept.
How would you propose to implement returns from an async code? Do you have examples of any async code returning values without “objects” or compiler magic? There’s probably some c trickery I’m not aware of! As I’m not familiar with c, I look forward to learning a new thing.

I think it is the wording, semantics. Replace “return” with “emit” and it all holds up.

> Do you have examples of any async code returning values without “objects” or compiler magic?

Lua coroutines represent a yieldable computation, but given a coroutine object, you can't await it or retrieve a return value from it. The only things you can do with a coroutine are check its status and resume it. So how do asynchronous computations return values in Lua?

It's simple: don't create an extra coroutine around it. If you're already in a yieldable coroutine, then you can call the yielding function directly. Nested yields will bubble up, and from the caller's perspective you just call a function and it returns a value.

Doing this from Lua is a bit of compiler/interpreter magic, but some modified Lua interpreters support yieldable C functions that can also perform yieldable calls [0]. This requires some due diligence on the part of the caller, making sure to bubble up yields from the inner function and also pass down resumes until it completes (you need to add a state for "this function call yielded"). But it uses no promises, futures or channels.

Of course, you can create a coroutine that wraps a yielding function and notifies a channel or callback once it's done. Those just aren't part of the language and aren't actually necessary for multitasking.

[0]: https://github.com/MCJack123/craftos2-lua/blob/89dcba94c28be... (lua_getctx returns whether you're being resumed from a yield, and expects you to have pushed your own state to the Lua stack if you need any.)