Dart’s built_value for Immutable Object Models

David Morgan
Dart
Published in
7 min readNov 30, 2016

Last week I wrote about built_collection. I finished by remarking that to really make use of immutable collections, you need immutable values. So here we are: built_value. This is the second major piece behind my talk at Dart Developer Summit (video).

Value Types

The built_value package is for defining your own value types. The term has a precise meaning, but we use it informally to mean types where equality is based only on value. For example, numbers: my 3 is equal to your 3.

Not only that: my 3 will always equal your 3; it can’t change to be 4, or null, or a different type altogether. Value types are naturally immutable. This makes them simple to interact with and to reason about.

This all sounds terribly abstract. What are value types good for? Well, it turns out: a lot. A whole lot. Arguably — and I do argue this, often — any class that’s used to model the real world should be a value type. Observe:

var user1 = new User(name: "John Smith");
var user2 = new User(name: "John Smith");
print(user1 == user2);

What should it print? Crucially, both instances are supposed to refer to someone in the real world. Because their values are identical they must refer to the same person. So they must be considered equal.

What about immutability? Consider:

user1.nickname = 'Joe';

What does updating a “User” nickname mean? It could imply any number of changes; perhaps the welcome text on my web page uses the nickname, and that should be updated. I probably have some storage somewhere, so that will need updating too. I now have two major problems:

  • I don’t know who has a reference to “user1”. The value has just changed under them; depending on how they’re using it, this could have any number of unpredictable effects.
  • Anyone holding “user2” or similar is now holding a value that’s out of date.

Immutability can’t help with the second problem, but it does remove the first. It means there are no unpredictable updates, just explicit ones:

var updatedUser = new User(name: "John Smith", nickname: "Joe");
saveToDatabase(updatedUser); // Database will notify frontend.

Crucially, it means changes are local until explicitly published. This leads to simple code that’s easy to reason about — and to make both correct and fast.

The Problem with Value Types

So, the obvious question: if value types are so useful, why don’t we see them everywhere?

Unfortunately they’re extremely laborious to implement. In Dart and in most other Object Oriented languages, a large amount of boilerplate code is needed. In my talk at the Dart Developer Summit I showed how a simple two-field class needs so much boilerplate it fills a whole slide (video).

Introducing built_value

We need either a language feature — which is exciting to discuss, but unlikely to arrive any time soon — or some form of metaprogramming. And what we find is that Dart already has a very nice way to do metaprogramming: source_gen.

The goal is clear: make it so easy to define and use value types that we can use them wherever a value type makes sense.

First we’ll need a quick detour to look at how this problem can be approached with source_gen. The source_gen tool creates generated source in new files next to your manually maintained source, so we need to leave room for a generated implementation. That means an abstract class:

abstract class User {
String get name;

@nullable
String get nickname;
}

That has enough information to generate an implementation. By convention generated code starts with “_$”, to mark it as private and generated. So the generated implementation will be called “_$User”. To allow it to extend “User” there will be a private constructor for this purpose called “_”:

=== user.dart ===abstract class User {
String get name;
@nullable
String get nickname;
User._();
factory User() = UserImpl;
}
=== user.g.dart is generated by source_gen ===class _$User extends User {
String name;
String nickname;
_$User() : super._();
}

We need to use Dart’s “part” statement to pull in the generated code:

=== user.dart ===library user;part 'user.g.dart';abstract class User {
String get name;
@nullable
String get nickname;
User._();
factory User() = _$User;
}
=== user.g.dart is generated by source_gen ===part of user;class _$User extends User {
String name;
String nickname;
_$User() : super._(); // Generated implementation goes here.
}

We’re getting somewhere! We have a way to generate code and plug it into the code we write by hand. Now back to the interesting part: what you actually have to write by hand and what built_value should generate.

We’re missing a way to actually specify values for the fields. We could think about using named optional parameters:

factory User({String name, String nickname}) = _$User;

But this has a couple of drawbacks: it forces you to repeat all the field names in the constructor, and it only provides a way to set all the fields in one go; what if you want to build up a value piece by piece?

Fortunately, the builder pattern comes to the rescue. We’ve already seen how well it works for collections in Dart — thanks to the cascade operator. Assuming we have a builder type, we can use that for the constructor — by asking for a function that takes a builder as a parameter:

abstract class User {
String get name;
@nullable
String get nickname;
User._();
factory User([updates(UserBuilder b)]) = _$User;
}

That’s a bit surprising, but it leads to a very simple syntax for instantiation:

var user1 = new User((b) => b
..name = 'John Smith'
..nickname = 'Joe');

What about creating new values based on old ones? The traditional builder pattern provides a “toBuilder” method to convert to a builder; you then apply your updates and call “build”. But a nicer pattern for most use cases is to have a “rebuild” method. Like the constructor, it takes a function that takes a builder, and provides for easy inline updates:

var user2 = user.rebuild((b) => b
..nickname = 'Jojo');

We do still want “toBuilder”, though, for cases when you want to keep a builder around for a little while. So we want two methods for all our value types:

abstract class Built<V, B> {
// Creates a new instance: this one with [updates] applied.
V rebuild(updates(B builder));
// Converts to a builder.
B toBuilder();
}

You don’t need to write the implementation for these, built_value will generate it for you. So you can just declare that you “implement Built”:

library user;import 'package:built_value/built_value.dart';part 'user.g.dart';abstract class User implements Built<User, UserBuilder> {
String get name;
@nullable
String get nickname;
User._();
factory User([updates(UserBuilder b)]) = _$User;
}

And that’s it! A value type defined, an implementation generated and easy to use. Of course, the generated implementation isn’t just fields: it also provides “operator==”, “hashCode”, “toString” and null checks for required fields.

I’ve skipped over one major detail, though: I said “assuming we have a builder type”. Of course, we’re generating code, so the answer is simple: we’ll generate it for you. The “UserBuilder’ referred to from “User” is created in “user.g.dart”.

unless you wanted to write some code in the builder, which is a perfectly reasonable thing to want to do. If that’s what you want, you follow the same pattern for the builder. It’s declared as abstract, with a private constructor and a factory that delegates to the generated implementation:

abstract class UserBuilder extends Builder<V, B> {
@virtual
String name;
@virtual
String nickname;
// Parses e.g. John "Joe" Smith into username+nickname.
void parseUser(String user) {
...
}
UserBuilder._();
factory UserBuilder() => _$UserBuilder;
}

The “@virtual” annotations come from “package:meta”, and are needed to allow the generated implementation to override the fields. Now that you’ve added utility methods to your builder you can use them inline just like you could assign to fields:

var user = new User((b) => b..parseUser('John "Joe" Smith'));

The use cases for customizing a builder are relatively rare, but they can be very powerful. For example, you might want your builders to implement a common interface for setting shared fields, so they can be used interchangeably.

Nested Builders

There’s a major feature of built_value you haven’t seen yet: nested builders. When a built_value field holds a built_collection or another built_value, by default it’s available in the builder as a nested builder. This means you can update deeply nested fields more easily than if the whole structure was mutable:

var structuredData = new Account((b) => b
..user.name = 'John Smith'
..user.nickname = 'Joe'
..credentials.email = 'john.smith@example.com'
..credentials.phone.country = Country.us
..credentials.phone.number = '555 01234 567');
var updatedStructuredData = structuredData.rebuild((b) => b
..credentials.phone.country = Country.switzerland
..credentials.phone.number = '555 01234 555');

Why “more easily” than if the structure was mutable?

Firstly, the “update” method provided by all builders means you can enter a new scope whenever you like, “restarting” the cascade operator and making whatever updates you want both concisely and inline:

var updatedStructuredData = structuredData.rebuild((b) => b
..user.update((b) => b
..name = 'Johnathan Smith')
..credentials.phone.update((b) => b
..country = Country.switzerland
..number = '555 01234 555'));

Secondly, nested builders are automatically created as needed. For example, in built_value’s benchmark code we define a type called Node:

abstract class Node implements Built<Node, NodeBuilder> {
@nullable
String get label;
@nullable
Node get left;
@nullable
Node get right;
Node._();
factory Node([updates(NodeBuilder b)]) = _$Node;
}

And the auto creation of builders lets us create whatever tree structure we want inline:

var node = new Node((b) => b
..left.left.left.right.left.right.label = 'I’m a leaf!'
..left.left.right.right.label = 'I’m also a leaf!');
var updatedNode = node.rebuild((b) => b
..left.left.right.right.label = 'I’m not a leaf any more!'
..left.left.right.right.right.label = 'I’m the leaf now!');

Did I mention a benchmark? When updating, built_value only copies the parts of the structure that need updating, reusing the rest. So it’s fast — and memory efficient.

But you don’t just have to build trees. With built_value you have at your disposal fully typed immutable object models … that are as fast and powerful as efficient immutable trees. You can mix and match typed data, custom structures like the “Node” example, and collections from built_collection:

var structuredData = new Account((b) => b
..user.update((b) => b
..name = 'John Smith')
..credentials.phone.update((b) => b
..country = Country.us
..number = '555 01234 567')
..node.left.left.left.account.update((b) => b
..user.name = 'John Smith II'
..user.nickname = 'Is lost in a tree')
..node.left.right.right.account.update((b) => b
..user.name = 'John Smith III'));

These are the value types that I’m talking about when I argue most data should be value types!

More on built_value

I’ve covered why built_value is needed and what it looks like to use. There’s more to come: built_value also provides EnumClass, for classes that act like enums, and JSON serialization, for server/client communication and data storage. I’ll talk about those in future articles.

After that I’ll dig into the chat example that uses built_value in and end to end system with server and client.

Edit: next article.

--

--