Anton Astashov's blog

Would be nice to write something clever here.

When Dart Is a Good Choice

| Comments

There was a link to flutter.io recently, which went to Hacker News, Reddit, etc. The project itself is on the very early stage, not even alpha, afaik, and it wasn’t an announcement, somebody just posted a link to flutter.io there.

The programming language they use for Flutter is Dart. And I was really surprised by the crowd responses related to Dart – there was so much hate. It seems like there is some confusion among people about what the language and its ecosystem is these days. Since I have some experience with it, I decided to share my thoughts on the language and its ecosystem, what it’s good for and what it’s maybe not the best choice for, again, from my experience.

So, from my perspective, the main selling points, things which are really nicely done in Dart, are:

Async

As I previously mentioned, Dart has really nice async support across its whole ecosystem. Futures and Streams were added to the SDK from the very early versions, so they became standard de-facto, and are used in all the places where asynchronoucity is expected.

I didn’t see any alternative implementations of Futures or Streams in the wild, everybody just uses the SDK’s ones. And majority of packages is built with async APIs, methods return Futures and/or Streams. This makes them nicely composable with each other.

If you’re building a front-end app, you’ll have all the DOM events wrapped into Streams in SDK. If you’re building a web-service, database drivers (e.g. MySql) return the Stream with results as a result of query execution. Loggers give you a Stream with all the log records. You can subscribe/unsubscribe to these streams, map/filter/fold them, etc. All that stuff provides a good foundation for building FRP apps in Dart.

Working with Futures also became pretty pleasant since Dart got async/await support. That allows you to write heavily async/concurrent applications, which look very synchronous. Futures, again, are everywhere – HTTP has Future-based API, unit tests cases will wait if the test returns a Future, web frameworks will also handle it properly if the request handler returns a Future, etc.

There are Zones, which nicely augment the async story of Dart. They give you the ability to wrap any piece of code into a “Zone”, which will keep the async context of that piece of code. So you can catch all the errors (even if they happened in the async part of it), store zone-local values (they will be accessible globally inside the zone), and do a bunch of other things.

Generally, debugging of async apps is painful, because stack traces are messed up, and it’s hard to figure out where we came from to the point where the exception happened. They may look like this:

1
2
3
4
5
6
7
8
Unhandled exception:
Uncaught Error: foo
Stack Trace:
#0      blah.<blah_async_body> (file:///Users/anton/projects/blah/blah.dart:7:3)
#1      Future.Future.microtask.<anonymous closure> (dart:async/future.dart:144)
#2      _microtaskLoop (dart:async/schedule_microtask.dart:43)
#3      _microtaskLoopEntry (dart:async/schedule_microtask.dart:52)
...

Which gives you very little information, just the actual line where the exception happened. You have no clue how the app got to that point. Luckily, there is stack_trace package, which has Chain.capture() method. It uses Zones under the hood, and if you wrap your code with it, the stack traces became way nicer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
blah.dart 7:3        blah.<async>
===== asynchronous gap ===========================
dart:async           _Completer.completeError
blah.dart 10:1       blah.<async>
===== asynchronous gap ===========================
dart:async           Future.Future.microtask
blah.dart            blah
blah.dart 14:15      main.<fn>.<async>.<fn>.<async>.<fn>.<async>
===== asynchronous gap ===========================
dart:async           Future.Future
blah.dart 13:17      main.<fn>.<async>.<fn>.<async>
===== asynchronous gap ===========================
dart:async           Future.Future
blah.dart 12:28      main.<fn>.<async>
===== asynchronous gap ===========================
dart:async           Future.Future.microtask
blah.dart            main.<fn>
package:stack_trace  Chain.capture
blah.dart 11:9       main

It adds some performance and memory penalty, but usually it’s neglectable, and it helps a lot with debugging.

So, IMHO, Dart is really good for building front-end apps or single-threaded back-end services. Unfortunately, multithreading support is pretty immature for now – there are actor-based isolates, but they are very slow (instantiating of an isolate takes almost a second (sic!), and the throughput is about 20000-30000 messages per second on my MacBook Pro).

Staticly typed

It’s nice that there’s no compile-time, with there is static analysis support. Despite the fact the type system is “optional and unsound”, not using type annotations for public methods and functions is considered a bad practice.

The fact the language has source based VM actually simplifies debugging a lot. Despite a decent debugger and other tools, the most convenient way of debugging programs for me is still just throwing print statements. Here, you can easily throw them into the source code of any package or into SDK itself, and immediately see them on page refresh or script restart.

Static analyzer is written as a package, and also there is a daemon (basically headless IDE), which uses it and provides API, which could by used by the IDEs to provide Dart support. The analyzer supports autocomplete, go to definition, find usages, refactoring, etc. This is really cool, because it simplifies writing tools, which require analyzing of the source code. Like:

Static analyzis allows to find a lot of errors, which would be found in runtime otherwise later by end users. The analyzis coverage is okay, but sometimes I’d like it to be stricter. There is a new “strong mode” option in the analyzer coming, which will be stricter, especially related to “dynamic” variables (where the type inference failed, or where it was specifically marked as dynamic), and I’m really looking forward to it.

There is one thing missing, though, and it really hurts. Unfortunately, there’s currently no method generics (just class generics). There is DEP for that though, and it seems like the Dart team is experimenting with them in the new JS transpiler (implemented as annotations for now though), which gives me some hope it will wind up to the VM later as well.

Tooling

Humans are weak. We have hard time keeping more than 7 things in our head simultaneously, we forget things to do, we make mistakes, and cannot fully keep big complex constructs in our head. Complexity of large applications easily go waaay over what average human’s brains can process and keep track of.

So, we rely on tools instead. The programming language should be expressive and allow to build powerful abstractions, which would help us to handle the complexity. But that’s not enough. We also need a code editor, which would help us to navigate through the thousands of lines of code, we need static analyzers, which will catch the errors we made early, we need linters or code formatters, which will make sure we write in the same style within the team, because keeping the code in the same styles reduces its complexity, etc.

Luckily, Dart’s tooling story is very good.

There are 2 main IDEs – Intellij IDEA and Github’s Atom. They are being developed simultaneously. I prefer IDEA for now, it works flawlessly, everything I need from IDE is there – go to definition, find usages, refactoring (I mostly use just renaming though), auto adding imports, integration with dartfmt, debugger, reliable search through class/method names, etc. The only missing thing for me – it doesn’t show a propagated type (if you rely on type inference). This feature is presented in Atom, but it’s missing other stuff, like debugger and search for class/method names. It seems like Atom is developing faster though, so we’ll see how fast it will get the debugger and the search.

There is dartfmt, which autoformats the code for you. It may not match your style preference, but I found it’s just easier to give up and let it style my code for me. A lot of tools also use dartfmt under the hood (e.g. code generator tool source_gen), so it kind of makes sense to use it, so your code and autogenerated code look similar.

There is Observatory, which allows to connect to running Dart processes and debug and profile them, and monitor the metrics.

There is my Crossdart Chrome plugin for Github, which adds ‘go to definition’ and ‘find usages’ functionality to Github’s tree views and pull requests, which is reaaally useful for code reviews.

There is pub, Dart’s package manager. It has pretty simple CLI API, but allows to do all the necessary things for managing dependencies. Seems to be heavily influenced by Ruby’s Bundler. One of the neat features – allows to override dependencies with local ones or just force some version (ignoring requirements of other packages). It’s very useful when your app is very modular, and depends on many internal packages.

There is Dartdocs, the documentation for all the packages on pub.

And there is Crossdart, the hyperlinked source code for all the packages on pub.

And a lot more.

Decent language

Though Dart is a bit conservative language (for my taste), it’s a quite decent OOP language. I like how the language encourages explicitness, provides a solid and well-structured SDK, and offers pretty logical and straightforward semantics. There are also some nice new features, like implicit interfaces (basically you can use any class as an interface), factory constructors, method cascades, etc.

There are also things that I’d expect from a modern language and are missing at the moment, like:

  • Method generics (you can only specify generics on a class)
  • Non-nullable types (like in Swift or Kotlin, String and String?)
  • Value objects and better immutability story overall. To be fair, it’s kind of possible right now, if you just create classes where all the fields are final and use something like vacuum_persistent for immutable maps/sets/vectors, but you potentially can still add mutable objects into your immutable hierarchy. You also have to implement equality, copy method, etc, every time. There are a bunch of packages, which solve that with the code generation (like this one), but it still looks like a workaround. I miss Scala’s case classes and Kotlin’s data classes :)

I should mention though that there are DEPs for Method Generics and for Non-Nullable Types, so there is a hope they will be added eventually.

Summary

From my experience, it was a breeze to use Dart for:

  • Developing large complex web applications working in browser. For example, Montage. Static typing, tools and analyzer made it way easier to create and then maintain that photo book editor.
  • Developing single-threaded services with async IO, which use MySQL, PostgreSQL, Mongo or RethinkDB. If your service is just fetching some data from a database and network, then aggregating it somehow and returning in JSON or HTML (which seems to be a majority of use cases), you can get pretty good performance even on one thread, since all your IO will be async and non-blocking from the top to the bottom, and at the same time won’t look like a callback hell.

What it’s potentially not good for:

  • Multi-threaded services – isolates are not there yet.
  • Something which needs MSSQL – seems like no driver for it
  • Obviously, if you’re tied to another platform, you may want to use something else (like, JVM)
  • There are not many packages on pub (just about 2000), so check first if you have everything you need there.

Choosing a language and a platform is always a tradeoff. There are some things I don’t really like in Dart, but given the decent staticly typed language with good semantics, which feels like interpreted language (because the VM runs the source code, not the bytecode), great tooling, and great async support – there are not many alternatives at the end.

So, check Dart out if you haven’t yet, maybe you’ll find it useful too.

Comments