Hacker News new | ask | show | jobs
by syntern 4196 days ago
This article feels like it is talking about the pitfalls of not using OO principles (e.g. types, encapsulation).

I won't shed a tear if callback-hells are replaced by proper async APIs (like in Stream and Future-based dart:async, or the Thenable in the new JS standards), but this article seems to be misalinged.

Downvoters: care to elaborate?

1 comments

I'll take a stab at elaborating.

The essay is about callback-based APIs, so types and encapsulation are out of scope from the start. It then expands the scope and observes that OO principles don't address the points covered. For example, it says that one of the "magic" requirements would be a way to state: "Do not add a callback that calls log() or acquires any locks held while log() is called." There is no type for that.

Other than pure functional code, there's no way to do that.

The essential equivalent for a multi-actor system is deadlock prevention. OO principles don't help there either.

You can get the problem with a stream. Consider a logger stream, where the listener opens a database connection to save the value then closes it, and the database adds 'open' and 'close' events to the logger stream. This will lead to an geometric explosion of events on the stream, because nothing at the API level says you shouldn't put those pieces together that way, other than the documentation.

I could go into the OO details of how this stream might be implemented in Dart, but really the OO nature of the stream API obscures the essential self-referential nature of the problem.

You can propose an equivalent counter-example if you want to demonstrate that those APIs really do solve the problem. I happen to agree with the well-written essay, and OO principles or "proper async APIs" solve nothing.

Promises are single assignment, so they solve the problem. As soon as they resolve, they're safe. Calls to them don't happen until they resolve. If you want to change the logger, you use streams, which will handle safe replacement for you (flatmap).

We do some crazy async HPC code, and during on-boarding, new members invariably get some initialization / error handling wrong with raw callbacks, and convert pretty quick to FRP after that. Unstructured async is crazy.

Yes, the example problem doesn't match to promises.

I don't understand the "change the logger" comment. The example was the logger sends a message to the listener, which opens a database handle, which sends a message to the logger, and repeat. There's nothing to changing. I don't see how FRP helps eliminate that cycle.

This might be what's happening, which would surface as no sensible declaration ordering:

//try making the logger first

var logger = require('myLogger')(backends);

var listener1 = require('mySync')(logger);

var backends = Rx.Observable.fromArray([listener1, listener2]);

//try making the backends first

var listener1 = require('mySync')(logger);

var backends = Rx.Observable.fromArray([listener1, listener2]);

var logger = require('myLogger')(backends);

From there, you'd switch to unsafe FRP methods (imperatively injecting into event streams, e.g., Subjects in Rx), which is the warning sign of weird cyclic behavior.

Further: the essence of OO is polymorphism, or late binding, which is to say that you're passing in objects to things that then call methods on those objects. That's a pattern of interaction that easily gives rise to all the problems the original article points out with "callbacks". In short, OO isn't a solution; it's a contributor to the problem.
> Consider a logger stream, where the listener opens a database connection to save the value then closes it, and the database adds 'open' and 'close' events to the logger stream.

Or just SSH to a server and run tcpdump without arguments :)