So in the last couple of articles in this series, we got a basic introduction to Records, which is a new built-in collection type coming in Dart 3.0, and how they make developers' life easy with their various applications like facilitating multiple returns and streamlining code using control flow and destructuring (via pattern-matching).
In this article, we're going to dig deep into pattern-matching and various scenarios where destructuring is a very handy tool under your belt. (Spoiler alert - Switch statement is gonna have God-like capabilities when coupled with pattern-matching)
So let's begin, shall we?
JSON Destructuring
You see pattern-matching doesn't only work for records. In addition to record patterns, Dart 3 also has support for map and list patterns which is very helpful in case we want to destructure a JSON.
final json = {'character': ['Komi', 16]};
final name = (json['character'] as List)[0];
final age = (json['character'] as List)[1];
Using destructuring:
final json = {'character': ['Komi', 16]};
final {'character': [name, age]} = json;
print(name); // name
print(age); // age
Validating JSON
The above code works well when you know the data has the structure you expect. But assume a more real word scenario. Most of the time if not all, the JSON will be coming over the network or database. We must validate it first before performing any destructuring.
So consider the above example JSON. If we want to perform validations on it using the traditional way, it might look something like this:
final json = {'character': ['Komi', 16]};
if (json is Map<String, dynamic> && json.length == 1 && json.containsKey('character')) {
final character = json['character'];
if (character is List<dynamic> && character.length == 2 && character[0] is String && character[1] is int) {
final name = character[0] as String;
final age = character[1] as String;
print('$name is $age years old.'); // Komi is 16 years old.
}
}
Let's see how this mess can be simplified to a large extent using Pattern-matching and the Switch statement.
Pattern-matching in Switch case
final json = {'character': ['Komi', 16]};
switch(json) {
case {'character': [String name, int age]}:
print('$name is $age years old.'); // Komi is 16 years old.
break;
default:
throw 'corrupted JSON';
}
Yeah. I'm not kidding. That's all we need to do!
Note: When a pattern is inside a case, we say it's refutable. It simply means before destructuring, the pattern can decide whether or not it accepts the incoming value.
Rules for pattern-matching (not exhaustive)
Constant patterns like 'character' in our example, match when the value is equivalent to that String.
Map and List patterns (which are essentially {} and [] ) match if the value is the right kind of collection and then they recursively also match against their sub-patterns.
Variable patterns will match any value that has the variable's type.
More magical stuff with Switch
Guard clauses
A Guard lets us evaluate an arbitrary expression to see if that case should match. If the expression is false, the case fails.
Note that it is different from embedding an if statement inside the case body because when the guard is false, execution will continue to the next case instead of jumping out of the entire switch statement.
final character = 'Tadano';
final context = 'with Komi';
switch (character) {
case 'Tadano' when context == 'with Komi':
print('TadanoHappy');
break;
case 'Tadano' when context == 'with someone else':
print('TadanoWorried');
break;
case 'Yamai':
print('Yamai');
break;
Logical operators & Relational Operators
Finally, with pattern matching, we can also use logical and relational operators for matching switch cases.
late String pronoun;
final String character = 'Komi';
switch(character) {
case 'Tadano' || 'Katai':
pronoun = 'He';
break;
case 'Komi' || 'Yamai':
pronoun = 'She';
case 'Najimi':
pronoun = 'They';
default:
throw 'Character not supported';
}
Switch expressions
Dart 3 has also support for switch expressions.
final socialAnxietyLevel = 8;
final reaction = switch(socialAnxietyLevel) {
0 => 'Cool and collected',
1 || 2 || 3 => 'Nervous but trying to stay calm',
>= 4 && <=6 => 'Starting to panic',
7 || 8 || 9 => 'Feeling overwhelmed and frozen',
_ => 'Unable to function due to extreme anxiety'
};
print(reaction); // Feeling overwhelmed and frozen
If-case Statement
Switch statements sometimes feel pretty heavy-weight. So, Dart 3 also has support for If-case statements that let us embed a single pattern inside the condition of an if statement.
final json = {
'character': ['Komi', 16]
};
if (json case {'character': [String name, int age]}) {
print('$name is $age years old.'); // Komi is 16 years old.
} else {
throw 'corrupted JSON';
}
}
Object Destructuring
We have seen how pattern matching and destructuring work well with Records, Lists, Maps, and even primitive values, but Dart is an object-oriented language. It will be nice if we can use this set of tools for any kind of object as well.
Well, Dart 3 has got you covered here also using object patterns!
class Character {
final String name;
final String gender;
const Character({required this.name, required this.gender});
}
final characters = [
Character(name: 'Komi', gender: 'girl'),
Character(name: 'Tadano', gender: 'boy'),
Character(name: 'Yamai', gender: 'girl'),
];
for(final Character(name: name, gender: gender) in characters {
print('$name is a $gender.');
}
// Komi is a girl.
// Tadano is a boy.
// Yamai is a girl.
When using the same variable names for destructuring we can even make it a bit shorter like this:
for(final Character(:name, :gender) in characters {
print('$name is a $gender.');
}
I guess that would be all for this article and as a matter of fact for this pattern-matching series for now. I might add more content to the existing articles or add some more in this series once Dart 3.0 is out, but until then be on the Dart side and Keep Fluttering!