Description
Are Components an IDL for something of an Actor model or Communicating Sequential Processes?
If so, I wonder if this is a confusing way to start thinking about the situation:
Components define actions, which combine object-oriented methods and events with a state machine. Data is passed into the component when an action begins, and passed out of the component when the action ends.
Seems like what you're saying is "imagine writing a generic RPC wrapper around the node.js model" — objects that have a lot of async methods obj.makeMeASandwich(function (refusal, food) {})
and emit events obj.on('ranOutOfBaloney', …)
.
I don't feel this is a good foundation to build on. It is not a satisfying model — perhaps practices like de-Zalgo were the first canary of this. Object-oriented programming has this "encapsulated state" fetish, and slathering a layer of "you may expect a response to this FOIA request at Some Later Date™" on top gets messy. (In practice, I don't think node.js suffers a lot from this because objects are generally stream-oriented, not heavily shared, and events kind of happen to get fired in some sort of sane fashion.)
Imagine a object that represents some remote state, in node.js. Say a it's database or to make it even more a propos, a GPIO pin that's on the other side of a network. Your interface is something like:
remotePin.set(bool, cb)
— sets the pin value tobool
, callscb(err)
after something goes wrong or when it is soremotePin.get(cb)
— callscb(err, bool)
after something goes wrong or we have the value locallyremotePin.on('change', …)
— emitted whenever the value changes
Seems simple enough, eh? Now what is the output of:
var remotePin = …;
remotePin.on('change', function (bool) { console.log("change to:", bool); });
remotePin.set(true, function (err) {
if (err) console.error("couldn't set true", err.stack);
else console.log("set to true");
});
remotePin.get(function (err, bool) {
if (err) console.error("couldn't get", err.stack);
else console.log("read as:", bool);
});
remotePin.set(false, function (err) {
if (err) console.error("couldn't set false", err.stack);
else console.log("set to true");
});
Is (the local implementation of) the remotePin interface allowed to optimize its communications, batching up the two sets and sending only one? In this case, the two sets are in the same tick of the runloop, seems fair enough. Maybe the pin communications actually happens over some sort of other polling cycle, every 25ms you always exchange what the value is and what it should be…do we emit 'change' event based on local toggles or on the remote values seen? What if something actually needs to toggle on and then off, I guess it needs to wait for the callback of the first of course, but what is the result of a get started sometime after the first set? Depending on the communications channel, it might make sense to have the 'change' events fired based the local calls to set method, or maybe to have the remote emit them after it sees the set, or maybe have the local emit them when it sees a value changed…
I don't know if this is the best example to explain with, but hopefully exposes some of the questions that are left unanswered by the model of "weakly ordered OOP".
And ugh am I tempted to just delete this whole post, because I don't really have a counterexample of "look how clean it is if you specify your contract solely in terms as typed messages" or whatnot.
Also, I'm struck by the fact that the "best" way to turn a pin on locally looks like:
*addressOfPin = 1; // LED turns on, consult datasheets for the precise timing curves
Whereas in some sort of "mailboxy communicating actor" Go/Erlang/Rust thing it might look like:
send (my_message_id, SET_STATE, 1) over channel_to_remote
await next_message on channel_from_remote:
next_message matches (ACK, my_message_id):
// LED has been on for "a while now" I guess, huzzah?
next_message matches *:
// hmmm, not sure what it's up to…maybe the next message will be our ACK? I dunno?
timed out:
// oh good grief is our link down, now what
Seems you have to optimize for one or the other (sync/cheap/reliable vs. async/expensive/unreliable). If you try to make remote look local you end up with e.g. these pros/cons. If you force yourself to always treat local as remote, you end up with inefficient callback hell type stuff.
To try solve this across all sorts of languages with all sorts of concurrency solutions…that seems really challenging…
Do it 👍