Replacing tuples with records
A refactoring use-case for dart's new language feature
With the release of Dart 3.0 came a new feature -- records. You can learn about records in the Dart language documentation.
For those unaware, to paraphrase: records are an anonymous, immutable, aggregate type. Records let you bundle multiple objects into a single object, but unlike other collection types, records are fixed-sized, heterogeneous, and typed. Records are real values; you can store them in variables, nest them, pass them to and from functions, and store them in data structures such as lists, maps, and sets.
A practical use-case
I had an occasion to use the new records feature in updating a widget. I thought I'd share where the records feature works and the benefits gained.
The widget is not important, but for reference, it's a table. The table has an undo feature for changes to rows.
In the original version, a tuple (package:tuple) with five dimensions was used to record the nature and details of a change to a row permitting a user to reverse the change if necessary. The tuples are stored on a stack, so undo is simply a matter of calling stack.pop()
. The old code looked something like this:
import 'package:stack/stack.dart';
import 'package:tuple/tuple.dart';
enum UndoAction {
created,
updated,
deleted,
reordered,
}
class UndoState {
// undo action, old state, new state, old index, new index
final state = Stack<Tuple5<UndoAction, RowState?, RowState?, int?, int?>>();
}
An example of creating a tuple and adding it to the stack looks something like this:
import 'package:tuple/tuple.dart';
...
onReorder: (oldIndex, newIndex) {
setState(() {
final tuple = Tuple(
UndoAction.reordered, // undo action
_tableSource.rows[newIndex], // old state
null, // new state (n/a)
oldIndex, // old index
newIndex, // new index
);
_tableSource.undo.state.push(tuple);
...
});
}
...
Notice the comments on each element to imbue its semantics.
Even worse is accessing the elements of the tuple:
void _undoRow() {
if (_tableSource.undo.state.isNotEmpty) {
setState(() {
// Retrieve the last action and delete it from the stack
final lastAction = _tableSource.undo.state.pop();
// Undo the last action
switch (lastAction.item1) {
...
case UndoAction.reordered:
final rowState = lastAction.item2!;
final oldIndex = lastAction.item4!;
final newIndex = lastAction.item5!;
_tableSource.rows.removeAt(newIndex);
_tableSource.rows.insert(oldIndex, rowState);
break;
...
}
});
...
}
}
The elements have to be accessed using the itemN
syntax. It's very unclear what the element represents in the structure, so appropriately named variables are required here.
Replacing with records
A simple swap in replacement of the tuple with a record looks like the following:
import 'package:stack/stack.dart';
enum UndoAction {
created,
updated,
deleted,
reordered,
}
final state = Stack<(UndoAction, RowState?, RowState?, int?, int?)>();
We immediately remove the need for the tuple package, so that can go.
And using it looks like this:
...
onReorder: (oldIndex, newIndex) {
setState(() {
final undoRecord = (
UndoAction.reordered, // undo action
_tableSource.rows[newIndex], // old state
null, // new state (n/a)
oldIndex, // old index
newIndex, // new index
);
_tableSource.undo.state.push(undoRecord);
...
});
}
...
And accessing the record fields is as follows:
void _undoRow() {
if (_tableSource.undo.state.isNotEmpty) {
setState(() {
// Retrieve the last action and delete it from the stack
final lastAction = _tableSource.undo.state.pop();
// Undo the last action
switch (lastAction.$1) {
...
case UndoAction.reordered:
final rowState = lastAction.$2!;
final oldIndex = lastAction.$4!;
final newIndex = lastAction.$5!;
_tableSource.rows.removeAt(newIndex);
_tableSource.rows.insert(oldIndex, rowState);
break;
...
}
});
...
}
}
Now we access the fields of the record using the $n
syntax.
Have we swapped out one evil for another? Not quite, we did remove a dependency remember?
Where records shine
Records have a compelling feature -- named fields. We can name the fields, and then access them using their field names.
Here is the updated code using named fields:
import 'package:stack/stack.dart';
enum UndoAction {
created,
updated,
deleted,
reordered,
}
class UndoState {
final state = Stack<
({
UndoAction undoAction,
RowState? oldState,
RowState? newState,
int? oldIndex,
int? newIndex,
})>();
}
An example of creating a record and adding it to the stack now looks something like this:
...
onReorder: (oldIndex, newIndex) {
setState(() {
final undoRecord = (
undoAction: UndoAction.reordered,
oldState: _tableSource.rows[newIndex],
newState: null,
oldIndex: oldIndex,
newIndex: newIndex,
);
_tableSource.undo.state.push(undoRecord);
...
});
}
...
And accessing the fields is as follows:
void _undoRow() {
if (_tableSource.undo.state.isNotEmpty) {
setState(() {
// Retrieve the last action and delete it from the stack
final lastAction = _tableSource.undo.state.pop();
// Undo the last action
switch (lastAction.undoAction) {
...
case UndoAction.reordered:
_tableSource.rows.removeAt(lastAction.newIndex!);
_tableSource.rows.insert(lastAction.oldIndex!, lastAction.oldState!);
break;
...
}
});
...
}
}
What an improvement! Comments and variable assignments are gone because it's now understandable. A dependency is gone, because records are now a first-class language feature. A big thank you to the Dart team.
After writing this article I decided to double-check my link to the tuple package in pub.dev. The following notice is present:
By and large, Records serve the same use cases that package:tuple had been used for. New users coming to this package should likely look at using Dart Records instead. Existing uses of package:tuple will continue to work, however, we don't intend to enhance the functionality of this package; we will continue to maintain this package from the POV of bug fixes.
Now, I understand some people will argue a class is a better structure for this, and they might be right. I feel however that a record works well in this instance, and happy to be enlighted with justified reasoning.
What do you think, are records a valid replacement for the tuple package here?