dart-lang / language

Design of the Dart language
Other
2.66k stars 205 forks source link

Automatically generate copyWith and copyWithout when class contains only public final fields that are initialized with named arguments. #961

Closed lslv1243 closed 7 months ago

lslv1243 commented 4 years ago

Suppose we have a class called Person:

class Person {
  final String name;
  final int age;

  Person({this.name, this.age});
}

Automatically generate the following methods:

Person copyWith({String name, int age}) {
  return Person({
    name: name ?? this.name,
    age: age ?? this.age,
  });
}

Person copyWithout({bool name = false, int age = false}) {
  return Person({
    name: name ? null : this.name,
    age: age ? null : this.age,
  });
}

The copyWith method is like the ones we already know present in flutter plain objects and the copyWithout is the solution I came up with to the problem of not being able to set null value using copyWith (when the user wants to copy something to null he uses the argument with value equal to true. This feature could be a opt in with a decoration in the class, and would have compile time analysis if the class matches the rules described in the title: A class that contains only public final fields that are initialized with named arguments.

The decoration could be something like:

@copyable
class Person {
  final String name;
  final int age;

  Person({this.name, this.age});
}

And the compiler would be able to generate the copyWith and copyWithout methods and also make then available in the autocomplete for good developer experience.

Then with this feature I would be able to do this:

@copyable
class Person {
  final String name;
  final int age;

  Person({this.name, this.age});
}

void main() {
  final person0 = Person(name: 'Person Name', age: 20);
  // name = 'Person Name', age = 20
  final person1 = person0.copyWith(name: 'Another person name');
  // name = 'Another person name', age = 20
  final person2 = person0.copyWithout(age: true);
  // name = 'Person Name', age = null
}

I believe this feature could help a lot in writing tests where we could have some complete filled object (with all fields set to a valid initial value) and wants to change just one field to test a condition. I really hope some sort of copying gets implemented. Because manually writing this methods feels wrong. Thanks in advance!

pedromassango commented 4 years ago

What if Dart add support of Data Classes and this should be part of it?

lslv1243 commented 4 years ago

@pedromassango I dont know exactly how a Data Class you talked about would behave. But if it just classes in the format that I described it could be classified automatically by the compiler without any special keyword.

mateusfccp commented 4 years ago

I don't think this belongs to the language specification. Maybe you could open an issue on the SDK side.

A language-side solution is to implement a parameter spread operator, like JS one, where things like this would be possible:

@copyable
class Person {
  final String name;
  final int age;

  Person({this.name, this.age});
}

void main() {
  final person0 = Person(name: 'Johan', age: 20);
  final person1 = Person(...person0, name: 'Feldispato');
}

This alternative has alredy been proposed (#893, maybe in other issues too).

pedromassango commented 4 years ago

@pedromassango I dont know exactly how a Data Class you talked about would behave. But if it just classes in the format that I described it could be classified automatically by the compiler without any special keyword.

Sorry. I mean Data classes from Kotlin (https://kotlinlang.org/docs/reference/data-classes.html)

eernstg commented 4 years ago

Cf. #125, #183, #314, at least.

vsevolod19860507 commented 4 years ago

Instead of "copyWithout" it would be better to use the following:

void main() {
  var user = User(name: 'Seva');
  print(user.name);
  user = user.copyWith();
  print(user.name);
  user = user.copyWith(name: null);
  print(user.name);
  user = user.copyWith(name: 'Seva2');
  print(user.name);
}

class Unused {
  const Unused();
}

const unused = Unused();

class User {
  final String name;

  User({this.name});

  User Function({String name}) get copyWith => _copyWith;

  User _copyWith({
    Object name = unused,
  }) {
    return User(
      name: name == unused ? this.name : name as String,
    );
  }
}
samandmoore commented 4 years ago

I'd really love for Dart to support the data classes and copy functionality outlined here in this post about C# 9. https://devblogs.microsoft.com/dotnet/welcome-to-c-9-0/

I've been building a lot of features in a flutter app recently, and I've found that the community and patterns have coalesced around immutable data types. As a result, we've found oursevles using freezed (https://github.com/rrousselGit/freezed) to provide data classes and "sum types" / "enums with associated data" since there's no story built into Dart for it yet.

The user-land codegen required to support this functionality today makes me sad. It has a lot of quirks and it doesn't interact great with IDEs. It would be game-changing if Dart provided these features out of the box.

esDotDev commented 2 years ago

Ya the new records from C# really make me jealous, it would be so amazing to just write: public data User(string firstName, String lastName)

And have equality, copyWith, toJson and fromJson all in one. Doing it with meta-programming works, but is far more cludgy with fragile part directives and .g files everywhere and a ton of needlessly generated boilerplate code. https://www.youtube.com/watch?v=9Byvwa9yF-I

vietstone-ng commented 8 months ago

I love to have this.

munificent commented 7 months ago

I'm going to close this out because I think it's mostly covered by other issues. We definitely know that many many users highly desire something like data classes in Dart, including some sort of record update (i.e. copyWith()) mechanism. We want it too!

It's mostly a question of figuring out which approach is the right one and balancing trade-offs. I don't think it's likely that we would implicitly add copyWith() to all classes that have only public fields initialized with named arguments. I think that would be a little too magical and not necessarily what the class author wants. Also, interactions with inheritance would be challenging.

pedromassango commented 7 months ago

The magic is wayyh better than having to manually implement a copyWith method for all entities.

Anyway, I agree with you and since we're getting Data Classes and they will come with a copyWith we're all good 👍