Thursday, January 14, 2016

Cleaning Up that Dart Isolate Mess


I am kinda OK with my current remote proxy pattern implementation in Dart. Sorta.

Actually, the proxy itself is decent, thanks in large part to the async / await syntax introduced the other night:
main() async {
  // Spawn remote isolate worker here...

  ProxyCar car = new ProxyCar(receiveStream, s);

  await car.drive();
  print("Car is ${car.state}");

  await car.stop();
  print("Car is ${await car.state}");
}
Futures and promises are nice constructs, but they sure can get noisy. Dart uses await inside of an async function as syntactic sugar.

That code looks so nice that the code responsible for spawning the isolate worker is bugging me:
main() async {
  var r = new ReceivePort();
  var receiveStream = r.asBroadcastStream();

  await Isolate.spawn(other, r.sendPort);

  SendPort s = await receiveStream.first;

  ProxyCar car = new ProxyCar(receiveStream, s);
  // ...
}
I think I would prefer my client code to look something like:
  ProxyCar car = new ProxyCar();
  await Isolate.spawn(other, car.sendPort);
  await car.ready;
  // ...
I initially try to jam all of the isolate communication code directly into the ProxyCar. As most would expect, that made for a messy ProxyCar. So instead, I define a Talker class to establish a ReceivePort in the current isolate, listen on that same ReceivePort for a SendPort sent by the spawned isolate, and a method for sending messages on that SendPort:
class Talker {
  SendPort _s;
  ReceivePort _r;
  Stream _inStream;
  Future ready;

  Talker() {
    _r = new ReceivePort();
    _inStream = _r.asBroadcastStream();
    ready = _inStream.first.then((message){
      _s = message;
    });
  }

  // For others to talk to us
  SendPort get sendPort => _r.sendPort;

  Future send(message) {
    _s.send(message);
    return _inStream.first;
  }
}
I find myself getting confused by the SendPort from the other isolate and the SendPort that this has to expose for the other isolate to communicate back. As a first pass, I make the SendPort from the other isolate private. It is only used internally as is the ReceivePort for communicating back to Talker. I like that except that the SendPort associated with the private ReceivePort is publicly available. So I get confused. I really want to find better names for these beasties, but I have to admit that "send port" and "receive port" capture the intent as well as anything. Oh well.

With Talker, I can redefine ProxyCar as:
class ProxyCar implements AsyncAuto {
  String state = "???";
  Talker _t;

  ProxyCar() {
    _t = new Talker();
  }

  Future drive() => _send(#drive);
  Future stop()  => _send(#stop);

  SendPort get sendPort => _t.sendPort;
  Future get ready => _t.ready;

  Future _send(message) =>
    _t.
      send(message).
      then((response){
        print("[ProxyCar] $response");
        state = response;
      });
}
That is still a little heavy on the communication side, but I can live with it as all of it delegates to Talker or to the proxied methods (drive(), stop(), etc.).

With that, my original goal is met. The client code becomes simply:
main() async {
  ProxyCar car = new ProxyCar();

  await Isolate.spawn(other, car.sendPort);
  await car.ready;

  await car.drive();
  print("Car is ${car.state}");
  print("--");

  await car.stop();
  print("Car is ${await car.state}");
}
While I like some of this approach, it is proving difficult to explain. I may have to revisit the example or the approach before it is ready for inclusion in Design Patterns in Dart.


Day #64

No comments:

Post a Comment