VideoBar

This content is not yet available over encrypted connections.

Friday, October 5, 2012

Common Dart Scenarios for the 'Future' API

A Future<T> object represents a unit of work that will complete at some point in the - you guessed it - future.  I use Futures a lot in my code and I thought that rather than do an exhaustive walk-through of the class itself, I'd share some of the more common patterns and concepts that I often find useful.

In Future Does Not Mean In Parallel

If you come from the .net world, you'll probably have some experience with Task<T> and the whole parallel processing API.  Futures aren't that.  They are more like structured callbacks, which will almost always run in the same memory space and thread as your application.  They won't run in their own threads or multi-core on their own.  If you're interested in parallelism, consider exploring Dart's Isolates API.

Futures Are Not Automatically Asynchronous

At the time of this writing, there is a ticket open in the Dart queue to make all futures asynchronous, but for now what you are about to read remains accurate.

It is important to understand that Futures are not necessarily asynchronous by default. Let's look at this example:
 // This future is not asynchronous and will complete as soon as it is called.  
 Future<bool> greaterThan42(int number){  
   final c = new Completer();  
   c.complete(number > 42);    
   return c.future;  
 }  

This above code is synchronous, in fact you can write it in a much more succinct way using the .immediate() constructor of the Future class:
 Future<bool> greaterThan42(int number) =>  new Future.immediate(number > 42);  

Futures are really most powerful when dealing with asynchronous work, lets use the Timer class to simulate that here:
 Future<bool> greaterThan42(num number){  
   final c = new Completer();  
   
   void doWork(_){  
    c.complete(number > 42);  
   }  
   
   // take 3 long seconds to return our result  
   new Timer(3000, doWork);  
   
   return c.future;  
 }  

The example above uses the Timer asynchronously callback a function after a 3 seconds has elapsed.

A Couple Of Common Future Scenarios

Waiting For One Or More Futures To Complete

The futures API provides a very convenient way to deal with this scenario, in Futures.wait().  Lets see how it works by modifying our greaterThan42 future a bit.
 Future<bool> greaterThan42(num number, int howLongMs){  
   final c = new Completer();  
   
   void doWork(_){  
    c.complete(number > 42);  
   }  
   
   // take 3 long seconds to return our result  
   new Timer(howLongMs, doWork);  
   
   return c.future;  
 }  
   
 main(){  
   Futures  
    .wait([greaterThan42(5, 1000), greaterThan42(100, 3000)])  
    .then((List<bool> results){  
      print(results);  
    }  
 }  

So what is happening above when we send a list of 2 futures into Futures.wait()?  Well, it waits!  It does all the mundane work necessary to be sure that all the futures in the list have completed.  It then itself returns a Future, containing a list of the results returned by the Futures we were waiting on.

The results of running the above code will yield:
 [false, true]  

Similar to this, we often need to iterate over a list of objects, where a method in each object returns a future. Dealing with this in a standard iteration construct won't work cleanly,  because the iteration will continue happily along whether or not our future has completed.  We can use Futures.wait() and a .map() to deal with this nicely:
 main(){  
   Futures   
   .wait(listOfFoos.map((foo) => foo.callFutureMethod()))   
   .then(results){  
     // now we can safely iterate the results of the future calls  
     results.forEach((bar) { // ... });  
   }   
 }   


Working with Futures in Sequence

Often times we want to receive the result of some Future, and then pass that result into another Future.  Do this 4 or 5 times and you get a really nasty scaffolding effect:
 callFutureA()  
   .then((resultA){  
    callFutureB(resultA)  
      .then((resultB){  
       callFutureC(resultB)  
         .then((resultC){  
           print('Kill me now!');  
         });  
      });  
   });  

Fortunately, the Future API gives us a nice way to deal with this as well, with Future.chain().  Future.chain() takes some result and then returns another future, such that you can keep your control flow flat:
 callFutureA()  
  .chain((resultA) => callFutureB(resultA))  
  .chain((resultB) => callFutureC(resultB))  
  .chain((resultC) => callFutureD(resultC))  
  .then((resultD){  
    print('Wait, don't kill me after all!');  
   });  

Furthermore, if you are just doing a simple transforms on a future, you don't have to wrap all of your transformational functions in Futures.  You can use the Future.transform() function to achieve the same result:
 futureA(String foo) => new Future.immediate('$foo A');  
 functionB(String foo) => '$foo B';  
 functionC(String foo) => '$foo C';  
 functionD(String foo) => '$foo D';  
   
   
 main(){  
  futureA('Transform me!')  
   .transform(functionB)  
   .transform(functionC)  
   .transform(functionD)  
   .then((result) => print('$result'));  
 }  

Yields:
 Transform me! A B C D

Now, I could go off on a tangent here about the need for an await keyword in Dart, and how that would make dealing with asynchronous control flows even more simple to work with, but I'll save that for another post (maybe).

Summary

The Future API provides a great way to manage asynchronous coding in Dart.  Smart use of the API will keep your asynchronous code well managed and readable.

1 comment: