Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Composability: From Callbacks to Categories in ES6 (medium.com/homam)
113 points by alrex021 on July 10, 2016 | hide | past | favorite | 34 comments


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.


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


As you start to look at these concepts, and Ramda, I highly recommend playing with ramda-fantasy[0] and the fantasy-land[1] spec that it implements. Super useful, and really made this sort of functional programming "click" for me (and, has really cleaned up some abstractions in a couple production projects, too!)

[0] https://github.com/ramda/ramda-fantasy

[1] https://github.com/fantasyland/fantasy-land


Here[0] is a Node REPL with rambda and rambda-fantasy functions as globals. It feels so fun to use, like a whole other language.

E.g. `map(x => x*2, [1, 2, 3])` (no need to write `R.map`)

[0] https://github.com/hemanth/ramda-repl


Note his call back hell is exacerbated by the unnecessary 'else' after checking for errors and returning.

I'm not sure that .bindTo() chaining reads any better than .then() chaining or async.waterfall().


The point isn't really to argue for another naming scheme for Promises. The construction of the Callback class is used to build up the reader's understanding the Monadic structure before introducing Categories and Kleisli Categories. The comparison of the structure of these abstractions comes up several times.

I think the final example (of myLongOperation) reads extremely well, and is pretty minimal, as the author states.


We have to handle errors at least one time during the operation. Both Promise and Callback classes propagate error in the computation and delay error handling till the last step (usage).

I agree that bindTo is not much different from then, but IMHO pipe and pipeTo are.


Looks like a new hell!


How is this a new hell? This is some elegant, mathematics-derived abstraction.


Elegance is in the eye of the beholder, and being mathematics-derived says nothing of its suitability for direct use by humans. Gödel numbering is also "mathematics-derived" but few people would regard it as particularly elegant or useful (it may be elegant in the mathematical purpose it serves, but not for engineering).


It will turn into a hell because JavaScript lacks Haskell's strong type system. The tooling will not give you early warning if you pass a plain function into a construct that expects a promise etc.


That's very true. But on the other hand dynamic typing is what enables us to play with JavaScript and re-create many concepts from FP in plain JS. This is not easy in many strongly typed languages which their type system falls short of Haskell's (that supports ADTs, kinds, recursive types, etc.).


Yes, dynamic typing allows experimentation. But just because you can make JS look like Haskell doesn't mean it's the most effective way to get stuff done. You'd be better off writing Haskell and transpiling it, IMO.

Note that I'm talking about writing an entire application in this style, not just sprinkling categories here and there.


I'm a static types kindaguy. So I've rewritten some of this code using Typescript to see what that looks like.

https://github.com/joost-de-vries/typescript-ramda/blob/mast...


This killed it for me:

class Callback

.....

this.then = g => new Callback(callback =>

Can't we just keep OOP out of FP?


Using class enables us to chain instances naturally in JS syntax, especially because JS lacks infix functions or custom operators. o1.then(f).then(g).run(cb)


You can avoid the class syntax here and write a function to return the same if you prefer.


I was recently thinking while working with ES6 Promises that I liked to have something equivalent to Haskells `do` notation. Or Scalas `for` comprehension.

Does something like that exist?

If not maybe I should try Kleislis instead. But I'd like my code to be accessible for my colleagues. And it's my rule of thumb that I try not to use point free constructions (like Kleislis) because a lot of colleagues will no longer understand my code easily.


On second thought: I guess Async Await are like a `do` notation specifically for Promises...


That's true. await sequence actions much like do notation.


..and it's flat instead of nesting.


The article didn't really sell that arrows have an advantage over monads as far as callbacks are concerned.


Do you have any particular use-case in mind for arrows in JS?


Really fun read!


Arent you guys around JavaScript tired of changing your programming paradigm every half a year?


Why would anyone made such a change every year? Many people write articles about better ways to do X in many languages but that doesn't mean you change everything to follow that today. Maybe it informs your decisions tomorrow, maybe it doesn't. Ultimately you don't need to do anything here.

Personally I don't use promises and won't use this either but that's only because I am working on a code base that needs to work in more places (so no ES6 for me and no I don't need the extra complexity in my build and debugging steps to do a transpile).


I'd love to know which runtimes do you have to support which don't support ES6 if you don't mind? I imagine you're dealing with old browsers.


At my previous job it was older browsers but my primary avoidance of ES6 is related to my open source project (msngr.js) where I want to ensure I support not only older browsers but also node.js 0.10. I could use ES6 and transpile with babel but I try not to do that when possible to keep the build process as simple as possible.

If there was a specific ES6 feature that I think would hugely benefit my productivity or if I was working on a project that could utilize ES6 in other ways I would certainly use it more.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: