Tuesday, January 19, 2016

Faking Dart Isolates for Proxy Pattern Profits


Perhaps the best answer is "not."

I have been struggling with how best to present isolates (isolated workers) as a remote proxy pattern vehicle. As of last night, I think I have the bare minimum of what I can do in my Dart code. It's good—functional, well named, proper—but still complex enough that it would distract from the main discussion.

So what if I get rid of isolates altogether? Well, for one thing, I likely would not really need a remote proxy pattern implementation. The main function could speak directly to the worker function without required go-betweens like send and receive ports. For the sake of argument and illustration, let's stipulate that there is a requirement for main and worker functions to speak only over streams.

I start by replacing the isolate / send-port / receive-post dance with two stream controllers in main()—one for sending messages to the worker function, the other for receiving messages from the worker:
main() async {
  var mainOut = new StreamController.broadcast(),
      mainIn = new StreamController.broadcast();
  // ...
}
Next, I create a remote ProxyCar instance, supplying these two stream controllers for communication with the real subject (which will reside in the worker() function):
main() async {
  var mainOut = new StreamController.broadcast(),
      mainIn = new StreamController.broadcast();

  // Create proxy car with send/receive streams
  ProxyCar car = new ProxyCar(mainOut, mainIn);
  // ...
}
This requires two minor changes to the ProxyCar declaration, neither of which should really affect ease of understanding. First, the _in and _out instance variables become StreamControllers instead of ReceivePort and SendPort. Second, I need to listen on the StreamController's stream instead of directly on the StreamController object:
class AsyncCar implements AsyncAuto {
  StreamController _out, _in;

  AsyncCar(this._out, this._in) {
    _in.stream.listen((message) {
      print("[AsyncCar] $message");
      if (message == #drive) _car.drive();
      if (message == #stop)  _car.stop();
      _out.add(state);
    });
  }
  // Proxied methods remain unchanged...
}
Back in main(), I also sent the mainOut and mainIn stream controllers to the worker:
  // Start "worker"
  worker(mainIn, mainOut);
Inside the worker() function, these two arguments are mirrors of the arguments in main()mainIn in main() is workerOut inside worker():
worker(StreamController workerOut, StreamController workerIn) {
  new AsyncCar(workerOut, workerIn);
}
The AsyncCar class requires the same minor StreamController changes that I made to ProxyCar, but the actual functionality remains unchanged from the isolate version of the code.

And that does the trick. Back in main(), I can invoke the usual vehicle methods on the ProxyCar and those requests are forwarded onto the real AsyncCar in worker():
main() async {
  var mainOut = new StreamController.broadcast(),
      mainIn = new StreamController.broadcast();

  ProxyCar car = new ProxyCar(mainOut, mainIn);
  worker(mainIn, mainOut);

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

  print("--");

  await car.stop();
  print("Car is ${await car.state}");
}
Along with some debugging code inside the car classes, this code produces the following output:
$ ./bin/drive.dart                          
[AsyncCar] Symbol("drive")
[ProxyCar] driving
Car is driving
--
[AsyncCar] Symbol("stop")
[ProxyCar] idle
Car is idle
I like that. The example is certainly contrived, but experienced Dartisans will recognize where this can go while folks new to the language should not be completely lost. Once the main discussion is done, I would then be free to show a quick code transformation into an isolate—or even a web socket—solution.

And best of all is that this approach can be seen on DartPad: https://dartpad.dartlang.org/7cc9f080e49939ac4cad.


Day #69

No comments:

Post a Comment