Open AlexVegner opened 1 year ago
I'm not seeing a lot of difference between this an inline classes, #2727, at least if we allow the inline classes to implement or extend their representation type (which I hope we will, and plan on pushing for).
The one difference I can see is that you want to override equality, which is not something an inline class can do, because it's entirely static, and not a real wrapper object. Since ==
is very often invoked as an instance method on an object with a generic type variable as type, there'd need to be some kind of runtime overhead in order to replace the object's own equals method.
@lrhn Thanks for response, hope inline classes will provide elegant solution for static record type.
I think it's interesting to take a look at the example here expressed using inline classes:
import 'dart:convert';
// ignore_for_file: unused_local_variable
typedef UserRecord = ({String? firstName, String? lastName});
inline class User {
final UserRecord value;
User({String? firstName, String? lastName})
: value = (firstName: firstName, lastName: lastName);
User.fromRecord(this.value);
String? get firstName => value.firstName;
String? get lastName => value.lastName;
Map<String, dynamic> toJson() => {
'firstName': value.firstName,
'lastName': value.lastName,
};
static User? fromJson(Map<String, dynamic> json) => switch (json) {
{
'firstName': String? firstName,
'lastName': String? lastName,
} =>
User(firstName: firstName, lastName: lastName),
_ => null,
};
}
inline class Employee implements User {
final UserRecord value;
Employee({String? firstName, String? lastName})
: value = (firstName: firstName, lastName: lastName);
Employee.fromRecord(this.value);
/* These members should be available due to `implements User`, but that
has not yet been implemented in the analyzer. So we can copy-paste them
if we need to get the analyzer to accept the program:
String? get firstName => value.firstName;
String? get lastName => value.lastName;
Map<String, dynamic> toJson() => {
'firstName': value.firstName,
'lastName': value.lastName,
};
*/
}
void main() {
final record = (firstName: 'A', lastName: 'V');
final user = User.fromRecord(record);
final json = jsonDecode('{"firstName": "A", "lastName": "V"}');
final u2 = User.fromJson(json)!;
final User(:firstName, :lastName) = u2;
final e1 = Employee.fromRecord(record);
final e2 = Employee.fromRecord(user.value);
// Knowing that these types are inline types, we can cast.
final e1worksButIsNotRecommended = record as Employee;
final e2worksButIsNotRecommended = user as Employee;
// No spread operator or `copyWith` methods, but today we can do this:
final e3 = Employee(firstName: 'D', lastName: e1.lastName);
final e4 = Employee(firstName: 'D', lastName: record.lastName);
final e5 = Employee(firstName: 'D', lastName: user.lastName);
assert(user == record); // Different type, same record.
assert(e1 == e2); // Same type, same record.
assert(e2 == user); // Different type, same record.
assert(e3 == e4); // Same type, equal records.
}
The big difference is that an inline class relies on a purely static distinction between the representation object (denoted by the unique final instance variable that every inline class must have) and the value whose type is the inline class: It's the same object at run time.
So we get ==
in the final assert
statements when the underlying record is equal, and record equality is based on per-field equality (so you can create a new record with equal field values, and they will be equal).
This illustrates that there is some manual work associated with an inline class whose representation is a record (for instance, we need to write String? get firstName
etc.), but it can be done, and the outcome is a type which has structural equality, statically resolved member implementations, and which allows compilers to box/unbox the object whenever that's good for the performance.
Adding static methods to existing classes is a better fit for static extensions (no such feature, yet, but it might look like this):
// app_colors.dart
import 'package:flutter/material.dart' as material;
static extension AppColors on material.Colors {
static Color get myBusinessColor => const Color(0x012345678);
}
Adding functionality to existing classes could be a good fit for a regular extension
declaration:
import 'package:openapi/openapi.dart' as openapi;
extension UserExt on openapi.User {
Map<String, dynamic> toJson() => {"firstName": firstName, "lastName": lastName};
}
Adding static methods to existing classes is still a static extension
thing:
static extension StaticUserExt on openapi.User {
static openapi.User? fromJson(Map<String, dynamic> json) {
if (json case {'firstName': String firstName, 'lastName: String lastName}) {
return openapi.User(firstName: firstName, lastName: lastName);
}
return null;
}
}
@eernstg Thanks for detailed explanation.
Record is a great feature, it brings us close to minimalistic data classes. It will be nice to have ability to create statically typed class records with with full potential of records. In additional to inline classes (https://github.com/dart-lang/language/issues/2727) it can provide a lot of new instruments.
Preliminary sintaxis:
It will bring all existing class functinnality + additional methods and static methods
Should NOT ALLOW:
Should ALLOW:
Sample for records:
Also it will allow to add static methods to abstract classes
And add functionality to extarnal pachage classes