Hacker News new | ask | show | jobs
by lmeyerov 3635 days ago
Interesting: as far as I can, this is promises, except there is a final strictness point at the end of the composition. That is, in turn, analogous to a one-shot form of old FRP formulations, where there is a final strictness/execute call. We already see that in JS libraries, like 'subscribe' in Rx: https://github.com/Reactive-Extensions/RxJS/blob/master/doc/...

FWIW, I'm actually not that thrilled about this sort of deferred approach. In Rx, that is part of the reason 'hot' vs 'cold' documentation has to exist, and I've seen many people struggle with it in theory & practice.

4 comments

This comment is correct - promises in JavaScript started like this and settled on their current API for ergonomics. Here is a standard promise implementation with deferred execution that never took off: https://github.com/then/lazy-promise

A lot of tough calls had to be made and pragmatic best interest of users typically triumphed over purity.

A promise is monadic if you drop exceptions and use the `then` overload which is `bind`. You have a `pure` - `resolve`.

The promise constructor has to exist to interop with callbacks but for no other reason really.

Come to think of it, the "strictness" of the Promise constructor is more ergonomical for concrete, day-to-day usage, where you're binding specific things together.

It became a pain point for me when using Promises as part of a larger abstraction, where the exact use was not known. Then I've worked around the strictness by embedding a "trigger" into a set of Promises so that I can set them off all at once, after they are all created.

Good article, btw. One of these days the Monad/Category lightbulb is going to go off for me, and this brought me closer to that using a familiar example.

Regarding monads, there's some discussion on whether promises really are monadic in JS here: https://gist.github.com/briancavalier/3296186

Basically the counter-argument is that promises are not composable, i.e. you can't wrap a promise in another promise.

There was an attempt to convince the spec authors to make promises "true" monads but it failed for reasons of simple practicality: https://github.com/promises-aplus/promises-spec/issues/94

IIUC, it's a bit different. Kleislis reify functions with type (A => F[B]), providing composition of these functions. (See http://underscore.io/blog/posts/2015/10/14/reification.html) Composition of Kleislis is like old style FRP in that it is synchronous.

However the hot/cold distinction in Rx is because execution is not deferred, as I understand it. IIUC observables / streams / whatever they are called in Rx start running as soon as they are defined. If `naturalNumbers` is the event stream of natural numbers, the result of

    naturalNumbers.map(x => print(x))
might not output 0, 1, ..., because a whole bunch of natural numbers might have already been emitted before `map` is called. It is this difficulty with reasoning that motivates hold/cold streams (again, as I understand it). A simple solution is to separate defining the network and running it. e.g. by requiring calling `run` method to get a result. Then substitution is maintained in the "world" prior to calling `run`, and this distinction is unnecessary.
The hot/cold distinction is indeed because most Rx Observables use deferred creation (cold) and most Rx Operators use deferred creation (cold) until the equivalent to a run is called (subscribe) making the Observable hot.

The complications to this that make for so much documentation come from the places where those "mostly" answers are wrong: Rx has ways to build hot Observables that are not deferred and begin immediately; Rx has a few rare exception Operators that can force an Observable hot, those confusing matters; finally, Rx cold Observables by default don't share deferred execution so multiple subscriptions create multiple "heat transition" side effects (ie, an observable is "run" every time it is subscribed). There are mechanics and operators in place to deal with all of these cases in Rx, but that adds to the learning curve of knowing when deferred execution takes place. (So yes the separation you describe exists between creating a network of cold observable and making them all "hot", but it does bring its own complications only furthering the need for the hot/cold distinction in cases where you are trying to avoid repeating things like side effects, which will happen when you aren't properly sharing the same "hot" source.)

So yes, Hot/Cold is entirely about deferred versus immediate observables/streams and both the surprises of finding a hot observable when you expected a cold one (as in the case of your example) or even a cold observable when you expected a hot one (side effects and memory leaks and "no execution" problems because you forgot to make anything hot).

I've been using async await or async yield. Never been more happier.
Your observation that categories are used in FRP is correct. Haskell's Yampa is built on top of Arrows that is an extension of categories: https://wiki.haskell.org/Yampa