An Overview of Dart, Part 2
- The Structure of a Dart Program / Concurrency in Dart
- The Object Model / Overall
In the first article of this series, we started to look at Dart, the new language for web development from Google. This article takes a closer look at some of the interesting aspects of the language and discusses where it differs significantly form JavaScript.
The Structure of a Dart Program
JavaScript completely lacks any kind of modularity. To combine multiple JavaScript files, you simply reference them all from your HTML file. Even this is problematic. JavaScript was originally designed, as the name implies, for scripting, and so executes code as it is inserted into the program. This means that code from the first JavaScript file can affect aspects of global state that will then alter the behavior of the second. If the first file takes ages to load, the browser must wait to interpret any of the subsequent ones.
Dart, in contrast, has an explicit concept of libraries. More importantly, it omits the idea of code that executes at the top level, rather than in an explicit main() function. This means that a browser with a native Dart implementation is able to parse and even compile Dart libraries as they arrive, irrespective of order (and potentially even in parallel) and so can achieve faster start-up times.
Every Dart program must contain one main() function, which is the program entry point. This gives a strict ordering of program execution, independent of the order in which libraries are loaded.
Concurrency in Dart
JavaScript originally had no concurrency support. Recent versions introduce the idea of a Web Worker, as a background task that runs independently of the user interface thread and provides message passing for communication, but has no shared state.
Dart provides isolates, which are a similar concept and, in the JavaScript-based implementation are implemented using Web Workers. The main() function starts running in one isolate, just as the main() function in C starts running in one thread. It can subsequently spawn more isolates, just as a C thread can spawn more threads.
Unlike threads, isolates have no shared state with their parent. As an implementation detail, they may share immutable state, but conceptually any data accessed by the new isolate must be copied into it.
If you're familiar with Go, then you will find Dart's concurrency model quite familiar (if you're not, then I would be remiss if I didn't encourage you to read my Go Language Phrasebook). Communication between Dart isolates happens via ports, which are similar to Go channels. Dart is rather more explicit about the directionality of channels, splitting their endpoints into SendPort and ReceivePort instances. Messages can flow down the channel in only one direction, but you can use a pair of ports for bidirectional communication.
You can send primitive values, lists, or maps across a port; and also SendPort instances. When you use the send() method on a SendPort, it implicitly creates a new channel and passes the sending end along with the message (you can also reuse an existing SendPort if you prefer). This means that you can always get a reply to any message that you send, and you can implement synchronous semantics fairly trivially.
There are also some very convenient helpers built on top of SendPorts for you. If you use the call() method on a SendPort, you get a future back. I've talked about futures before a number of times—they're a very powerful and simple concept for concurrent programming. The future is a token that you can keep and then block when you actually need the result.
Futures in Dart are not quite as transparent as in other languages. They aren't proxies, so you can't just use them as if they were, but you can use them for simple synchronization. The then() method on a future takes a function as an argument, which will be passed the result of the future as soon as it completes. You can also use the chain() method to join together a sequence of asynchronous computations.
The last case seems pointless because it effectively means doing the work in a single thread, but it can be very useful for some common concurrent design patterns. For example, you can keep shared data inside a set of isolates and use chained futures to communicate with all of them.