Mitranim
Not sure what you mean by "simple examples without error handling". Just like normal synchronous code, promises and coroutines use exceptions, which have the nice property of composability: one catch for several statements/promises. So error handling is the same as in synchronous code.
Now granted, there's a LOT of async/reactive problems where sequential abstractions like promises are irrelevant. GUI programming comes to mind, where you want the view to reflect some reactive state that changes in arbitrary order.
Kind of answered in another subthread: https://news.ycombinator.com/item?id=14963524
Multicast cancelation based on refcounting doesn't look right to me. I explicitly avoided this in Posterus, sticking with exclusive ownership, what GTOR calls unicast (thanks for linking). The reason is that subscribers may come and go over time. I've seen, and written, code that caches a promise and reuses it many times. It may start with one consumer, lose it, be canceled, then another consumer comes and gets rejected. This just feels like a big footgun. In Posterus, multicast is explicit and opt-in (`.weak()` futures).
If we eventually standardise a solution, I'd like something with as few interfaces as possible. In a dynamic language, you don't have the luxury of creating a future that initialises into a task, returning a cancelation token. Too many types without static checking is just a footgun. This is part of why Posterus only has futures, why cancelation is provided in the future constructor and tied to the future itself, and why they're eager rather than lazy. Deviating from these constraints would introduce new types, increasing the footgun potential.
Bundle size: a typical SPA imports multiple libraries, they import more libraries, and so on. It's not just Rx. I shouldn't have to explain how small things add up. Not caring about size is how it balloons up. We have to care about it in every library.
But why start with the superset? I don't think it's good design. Consider the perspective of a language designer. You want to start with primitives that are as simple as possible while satisfying a vast number of real use cases. One-short async primitives are an important intermediary step towards observables, it shouldn't be skipped. Observables should be implemented in terms of these.
This layered design gets you a smaller cognitive cost of entry (promises are hard enough as it is!) and higher efficiency for the vast number of cases that don't need stream-like functionality.
On top of that, one-shot primitives are conceptually compatible with blocking expressions in coroutines such as async/await, whereas streams are not.
I don't get it when people shoot for fat primitives that do it all.
`arrive` — no particular reason. It's one "errback" method rather than two methods like `resolve/reject`, so it needs to have a neutral tone. Not too happy with it, better suggestions are welcome.
[1] Basic implementation of automatic resource management in JS: https://mitranim.com/espo/#-agent-value-
People gave a few examples in this subtopic: https://news.ycombinator.com/item?id=14962684
People gave a few examples in this subtopic: https://news.ycombinator.com/item?id=14962684
Example with Posterus:
async function main() {
try {
await Future.fromError('fail')
}
catch (err) {
console.error('caught:', err)
}
}
In Node, this actually produces an unhandled rejection because Posterus's scheduler uses `process.nextTick` and squeezes into this unnecessary delay. Doesn't happen if you `.catch()` manually or just use Posterus coroutines instead of interoping with async/await, but it highlights the incorrect implementation of async/await in the first place. (Or is the spec at fault?)A well-designed tool should minimise the amount of states the program can be in. Asynchronous programming is already horrifically bad, it generates combinatorial explosions of intermediary states. Adding imperative, mutative programming and an event-driven API only exacerbates the problem, increasing the amount of possible state sequences even further. I don't think xtream provides a good API.
Even better, they come with generator-based coroutines that work with futures, and are automatically cancelable: https://github.com/Mitranim/posterus#routine
You can even run it with Koa 2 instead of async/await: https://github.com/Mitranim/koa-ring
Level 0: one-value operations (promises = futures).
Level 1: multi-value operations (streams = observables), implemented in terms of one-value operations. Tokio did it well for Rust (https://tokio.rs).
Starting at level 1 doesn't feel right to me.
There are also different types of observables to consider. Rx, xtream, Most.js, they're basically stream libraries. When doing GUI programming, it's more useful to have reactive units with a _synchronous_ data access, because it matches how you want to access the data when drawing your view (e.g. in React). I ended using observables that look like Clojure's atoms (see Espo: [2]), with a special adapter for React (see Prax: [3]). Observable libraries like Rx were useless for that use case.
[1] Example of decent future/stream design: https://tokio.rs
[2] Observables focused on synchronous access: https://mitranim.com/espo/#-atom-value-
[3] React adapter for implicit reactivity, based on synchronous observables (impossible with async streams): https://mitranim.com/prax/
* Server. Request handler starts expensive work. Let's say 4-5 database requests, some FS operations, rendering and sending response. Client disconnects. Running those operations will waste resources, we should stop. [1]
* Web. You can make at most 6 concurrent HTTP requests. They're precious resources. Dropping a request you no longer need will let others complete. It's nice to be able to abstract a painful ajax API behind something like a promise that doesn't lose the important ability to abort.
* React. View instance starts async fetch that eventually updates its state. User navigates to another view before it finishes. Updating the state after the component is unmounted is an error, and React will rightly complain. We should stop it.
* Server: abstracting an operation that's already cancelable behind a future. Let's say using Electron to render a website into a PDF. It's a really expensive operation, and the API is a pain to program against. You want to provide something as simple as a promise, but that doesn't lose the ability to stop it. [2]
[1] I write servers using Posterus coroutines, so all async code is automatically owned and canceled where appropriate: https://github.com/Mitranim/koa-ring
[2] This relies on futures to abstract away tricky timing management: https://github.com/Mitranim/epdf/blob/1c54481d4760a7eb730eb3...
Short answer: observables do too much, and Rx is WAY, WAAAAY too large. We need simple async primitives before layering bigger abstractions on them. They shouldn't take encyclopedic amount of reading to learn. They need to come in a library that doesn't weigh over 100 KB.