Tuesday, January 12, 2016

Await Async Code Cleanup in Dart


My remote proxy pattern solution in Dart got a little off the rails last night. It worked, but the interface changed from a synchronous real subject to an asynchronous proxy subject. Worse, the code was a heap o' Futures.

I do not think there is anything I can do about the asynchronous proxy API—that is simply the nature of remote calls. I do think I can adapt the synchronous interface to an asynchronous interface, then proxy the asynchronous interface. I will investigate that tomorrow. First, I want to clean up the Future heap:
  Isolate.
    spawn(other, r.sendPort).
    then((_) => receiveStream.first).
    then((s) { car = new ProxyCar(receiveStream, s); }).
    then((_) { car.drive(); }).
    then((_) => receiveStream.first).
    then((_) { print("Car is ${car.state}"); }).
    then((_) { car.stop(); }).
    then((_) => receiveStream.first).
    then((_) { print("Car is ${car.state}"); });
That makes sense if you noodle it through—at least I could rationalize it yesterday. But it is ugly to read, hence ugly to maintain. At first glance what stands out is that some then() methods return values and one accepts a value returned from the previous future. It is not clear why—at least not without noodling. And all the while that noodling is taking place, I would not be thinking about the actual business value of the code, which resided in the ProxyCar object which is manipulating a real car in another isolate.

So, as the esteemed Kasper Lund suggested (challenged? cajoled?) in last night's comments, I ought to make use of Dart's async / await functions.

I start by marking the main entry point of my script as async:
main() async {
  // ...
}
As the name suggests, this indicates that the code inside is asynchronous. More specifically, it says that some code will return futures—futures that would otherwise need to be chained to ensure that they run in the expected order. The first future is returned from the isolate spawned to perform independent, real car work. Above, I waited for it to be ready with a then(). Now I can await it:
main() async {
  var r = new ReceivePort();
  await Isolate.spawn(other, r.sendPort);
  // ...
}
Since this main() function is marked async no other code in here will run until Isolate.spawn()'s future completes. Exactly as with the then(), but without the mess.

Next up is one of the mysterious return value futures. Previously, I waited for the first message to come back from the isolate with then((_) => receiveStream.first). That first message was the isolate sending a SendPort back to the main() execution worker so that code in main() can send messages back to the isolate. The hash-rocket return value returns the message so that the next future completes with its value.

Thus, the following two lines get a SendPort from the isolate to enable communication into the isolate and gives it to the ProxyCar:
    // ...
    then((_) => receiveStream.first).
    then((s) { car = new ProxyCar(receiveStream, s); }).
    // ...
This is hard to explain. It is hard to read. It is going to cause problems as the code evolves.

All I want is a SendPort from the isolate. With await, this is written:
  SendPort s = await receiveStream.first;
Once the value is ready, assign it to the local s variable. Easy-peasy! Then on the next line I create my proxy car just as if this were procedural code:
  car = new ProxyCar(receiveStream, s);
That is much easier to read.

I can then drive my car (which proxies a drive request to the real car in its isolated worker environment):
  car.drive();
No awaiting is needed for either of these as I am just sending messages. The only reason it was needed in the future heap was because everything else was in that mess. Thanks to async and await, that mess is gone.

Things are not completely rosy, however. Even await is not going to convert that drive() method into a synchronous call. The interface implemented by both the real Car and the ProxyCar expects drive() to return void:
abstract class Automobile {
  String get state;
  void drive();
  void stop();
}
So the drive() is going to send a message, then allow execution to continue right on to the next statement without waiting for the real car to start driving or to update the proxy car's state. Printing the car's state right away would result in believing the car is stopped:
  car.drive();
  print("Car is ${car.state}");
  // State hasn't had a chance to update and would report "idle"
With the current interface, I cannot await drive()—I need a Future. So, temporarily, I reach under the ProxyCar covers and await a message (a state update from the real car) to its receiveStream:
  car.drive();
  await receiveStream.first;

  // Proxy car state is ready, so print
  print("Car is ${car.state}");
As I mentioned earlier, I will pick back up tomorrow converting the proxied interface into an asynchronous version of the current synchronous interface. For now I note that, even with the reaching-under-the-covers (which I was doing in the future-heap code anyway), my remote proxy code is already far more readable:
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);

  car.drive();
  await receiveStream.first;

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

  car.stop();
  await receiveStream.first;

  print("Car is ${await car.state}");
}
That lovely win makes for a fine stopping point tonight. More async adapters tomorrow!


Day #62

No comments:

Post a Comment