Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add data classes #314

Open
ranquild opened this issue Oct 31, 2017 · 238 comments
Open

Add data classes #314

ranquild opened this issue Oct 31, 2017 · 238 comments
Labels
brevity A feature whose purpose is to enable concise syntax, typically expressible already in a longer form data-classes request Requests to resolve a particular developer problem

Comments

@ranquild
Copy link

Immutable data are used heavily for web applications today, commonly with Elm-like (redux, ngrx, ...) architectures. Most common thing web developer is doing with data is creating a copy of it with some fields changed, usually propagated to the root of state tree. JavaScript has spread operator for this. There should be a easy way to use immutable data structures in Dart. I would like to have data classes (inspired by Kotlin) in Dart. Possible API:

data class User {
  String name;
  int age;
}

Compiler assumes that all fields of data class are immutable. Compiler adds equals implementation based on shallow equals, hashCode implementation based on mix of all object fields, toString implementation of the form <Type> <fieldN>=<valueN>, and copy function to recreate object with some fields changed.

You may argue that there is already Built value package that allows to do similar things, but we have many issues with package, mainly:

  1. It requires writing a lot of boilerplate code
  2. It requires running watcher/manual code generation during development.
  3. It requires saving generated files to repository because code generation time is too large for big applications.

I have found that using built value actually decreases my productivity and I do my work faster with even manually writing builders for classes.

If data classes would be implemented on language level, it would increase developer productivity and optimizations can be made when compiling code to particular platform.

@eernstg
Copy link
Member

eernstg commented Oct 31, 2017

No promises, but it's on the radar..

@freewind
Copy link

freewind commented Apr 6, 2018

Or inline make it shorter?

data class User(String name, int age)

@dcovar
Copy link

dcovar commented Apr 7, 2018

Are there any updates on this enhancement? I'm currently working with Flutter, and having come from the Kotlin/Android world, this is something that would make the transition a lot nicer. Especially when creating ViewModels, or even simple data models, this would make it a lot easier.

@zoechi
Copy link

zoechi commented Apr 8, 2018

@dcovar don't expect anything short term. It won't be part of Dart 2.
They might tackle it after Dart 2.
The built_value package works well enough for me.

@fmatosqg
Copy link

fmatosqg commented Apr 29, 2018

Community could write a package similar to Lombok who autogenerates code from a valid annotated source code file.

https://projectlombok.org/

One more thing for the wishlist on either flutter/flutter#13607 or flutter/flutter#13834, not sure which

@zoechi
Copy link

zoechi commented Apr 30, 2018

@fmatosqg https://pub.dartlang.org/packages/built_value

@Cat-sushi
Copy link

User should have implicit constructor const User({this.name, this.age});, correct?

@saolof
Copy link

saolof commented Dec 25, 2018

One thing worth mentioning is that data classes and sealed classes can both be viewed as an instance of a metaclass. If enough different kinds of special-cased classes are proposed, at some point it might become better to add metaclass programming to the language, with a few individual cases of syntax sugar.

Dart already kind of flirts with the idea when you look at what was needed to make the mirrors API work.

@andrewackerman
Copy link

andrewackerman commented Apr 11, 2019

I support this, but suggest also adding toJson and fromJson methods to the generated code so data class instances can be easily (de)serializable.

@ivnsch
Copy link

ivnsch commented Apr 12, 2019

@andrewackerman Data classes shouldn't have more than a generic minimum to be used as domain entities, this being equals/hashCode, copy and toString. Serialization isn't a universal requirement and even less to an industry standard (which not necessarily everyone wants to use / can become outdated) like JSON.

@eernstg
Copy link
Member

eernstg commented Apr 12, 2019

For current activities in this direction, you may want to consider also issues like the following: #117, #125, #225, #308.

@andrewackerman
Copy link

@i-schuetz Then maybe there can be some optional attributes that can be added to the class declaration so these features can be added for people who need them? Serialization may not be a universal requirement but I'd bet that it would be needed often enough that people would want to at least have the option. Otherwise it would largely defeat the purpose of having a concise data class declaration syntax but then have to manually create the (de)serialization methods.

And it's not like it would need to serialize to actual JSON strings. It could serialize to Map<String, dynamic>, which is itself easily convertible to and from JSON.

@ivnsch
Copy link

ivnsch commented Apr 12, 2019

Maybe something generic along the lines of Swift's Codable could make sense, but this is an entirely different feature. Although equals and toString are convenience as well - I remember for example in Haskell this being solved via type classes (which to be implemented require only to write a word practically). I don't know which exact considerations Jetbrains did to shape data classes the way they did in Kotlin. It's probably something along the lines that equals and toString make sense always. Serialization, as you say, it's used only "often".

@dcov
Copy link

dcov commented Apr 12, 2019

I agree with @i-schuetz that adding a Codable protocol is probably the best option. It could even make its way into Flutter plugins (and the framework itself), where the data you pass to the 'other side' has to be encoded first.

@kevmoo
Copy link
Member

kevmoo commented Apr 12, 2019

@leafpetersen @munificent @lrhn – should be moved to the language repo?

@leafpetersen
Copy link
Member

@leafpetersen @munificent @lrhn – should be moved to the language repo?

yes, thanks.

@kevmoo kevmoo transferred this issue from dart-lang/sdk Apr 12, 2019
@FullstackJack
Copy link

Let's kill the argument that moving to Dart (Flutter) from Kotlin is like moving back in time several years.
https://medium.com/@wasyl/kotlin-developers-thoughts-on-dart-1f60c4ad21ad

@swavkulinski
Copy link

swavkulinski commented May 15, 2019

The point of having data classes (apart from immutability) is to have implicit methods e.g. toString(), hash(), == for free.

More importantly for immutable class there is a need for mutation method (e.g. Kotlin apply() aka copyWith() in other languages) with default implementation to avoid boilerplate of mutation method.

@benoit-ponsero
Copy link

Hello,
I'm looking for dart, the typesystem is great, it's well thought but i think there is a lack of functionnal support.
I'm a scala developer and we have "case class" for this.
It's provide toString, equals, hashCode and a copy method with optional params.

This proposal is great and could attract more developer like me. Do you know when it could be implemented ?

@jodinathan
Copy link

@benoit-ponsero is the method copy done by reflection?

@benoit-ponsero
Copy link

It's a compiler generated method.

@jodinathan
Copy link

Then it must be tree-shaken when built with dart2js.

@agusbena
Copy link

agusbena commented Jun 6, 2019

Here in our company we are crossing our fingers to get this feature arrive son!
Please!

@lrhn lrhn added the request Requests to resolve a particular developer problem label Jun 27, 2019
@Jonas-Sander
Copy link

I'm also hoping that this will be added, a way to have a default implementation of ==, hashCode and toString would make many things much easier and faster.

@MarcelGarus
Copy link
Contributor

For a more lightweight alternative to built_value, which will be syntactically closer to possible language-level data classes, I implemented a package data_classes.
Basically, you write a mutable class (like MutableUser) and it generates the immutable pendant (User) with a constructor, converters to and from the mutable class, custom ==, hashCode and toString() implementations, and a copyWith method.

@josiahsrc
Copy link

I find myself needing this every day. Our application has become too large for code generators. When we run build_runner in watch mode and make a change to the code, the dart analyzer will grind to a halt and take 5-minutes to re-analyze the project. Intellisense stops working during this time. I've tried to keep working through it, but each time I type it seems to set the analyzer back another minute. Huge blow to productivity.

I support the proposed design in the oc. Additionally, I think it would be tremendously valuable if data classes supported reflection. This way developers can write extensions for [to|from]Json or what have you, without needing to ship them out the gate. Not sure how feasible this is.

Looking to help out with implementation any way I can.

@lucavenir
Copy link

lucavenir commented Mar 20, 2024

To anyone else experiencing these struggles I want to share some tips I've found along the way of solving the problem stated in @josiahsrc's comment. I think it relates to this issue since - in the end - data classes will be "made with macros".

Indeed, if macros will ever be available, I'd still expect analysis solve this, but AFAIK macros just "move" the generated code away from build_runner at comptime, so analysis still needs to process the generated code.

Therefore, it might be worth to apply the following, depending on your case.

Split your code into packages

Depending on your architecture, chances are at a macro level that your code can be split into "external details" vs "internal details" (i.e. the infamous "data layer" from architectural literature).
A good practice is to move and abstract away this layer out (via a class, typically a "repository) in a pure dart package.
You can orchestrate your dependencies and add build_runner scripts using melos (link).
Like that, this "data package" will enclose all the serializations you need (typically, freezed + json_serializable).

Minimize dependencies on the .part files

build_runner caches the generated bits, based on what they import; therefore, if your file (which contains the part something.g.dart; instruction) imports a lot of libraries / dependencies, build_runner must re-run for any of those change. Things escalate quickly on a non trivial codebase. Check out this tweet on this.

Help the builder via build.yaml

Did you know? You can specify which files should be watched by which builder in your build.yaml. One way to do this is to specify some file/classes naming convention in your codebase. Or, you can just set up a single folder in which e.g. all your api models (dto) are generated, etc.

Example:

targets:
  $default:
    builders:
      freezed:
        # specifies the files that contain data classes
        generate_for:
          - lib/data/**/*.api.model.dart
          - lib/data/**/*.db.model.dart
          - lib/**/*.model.dart
      go_router_builder:
        # specifies the single file that should be watched for routing purposes
        generate_for:
          - lib/router/routes_configuration.dart
      json_serializable:
        generate_for:
          - lib/data/**/*.api.model.dart
          - lib/data/**/*.db.model.dart
      riverpod_generator:
        generate_for:
          - lib/router/router.dart
          - lib/data/**/*.api.dart
          - lib/data/**/*.db.dart
          - lib/data/**/*.cache.dart
          - lib/src/**/*.repository.dart
          - lib/src/**/*.provider.dart
          - lib/src/**/*.controller.dart
      # ... etc.
      reactive_forms_generator:
        generate_for:
          - lib/src/**/*.form.model.dart

@jakemac53
Copy link
Contributor

jakemac53 commented Mar 20, 2024

Regarding build_runner specifically, it causes a lot of churn for analysis because it continuously outputs files to disk. And for each of those changes the analyzer immediately analyzes it, and re-analyzes everything that imports it. So it can end up re-analyzing the entire world many times during a build_runner build in pathological cases.

This problem shouldn't exist with macros, @scheglov can confirm that. There was an idea a while ago to make build_runner wait and write out all files at the very end, instead of outputting them to disk as it goes, I think @simolus3 had done some work to this end. It might be worth trying to pick that back up, as doing all these edits "at once" (or as close to that as possible) should help the analyzer. Especially if we could avoid doing the delete->write loop at all, in the case that the contents don't change.

@josiahsrc
Copy link

josiahsrc commented Mar 20, 2024

@jakemac53 @lucavenir I've seen those steps help analysis quite a bit. Also, writing all the files at once sounds far more performant. But, I guess, to what end? Projects will only grow, and it feels silly to need to restructure code just to make the analyzer happy. Build runner optimizations seem like a bandaid fix to the root problem here (the need for data classes). It would be excellent to be in a world where code generation isn't required for data types.

@lucavenir
Copy link

@josiahsrc it's been argued, up here, several times, that a single data class hard-implementation on dart might be limiting. Macros, instead, enable this problem to be solved without upsetting Dart itself. You can learn more on this motivation document.

imho, if analysis / build times problems are solved with macros, it's gg, there's nothing left to fear.

@josiahsrc
Copy link

josiahsrc commented Mar 20, 2024

@lucavenir Thanks for sharing that. After reading through it, I'm in full support of macros. Feels like a superset of data classes. I see the challenges listed, do you know the status on this feature? Eager to help out here if I can

@jakemac53
Copy link
Contributor

I see the challenges listed, do you know the status on this feature?

We are actively working on the implementations now (it was on hold for a while for patterns/records and while we were finishing up the null safety migration of Google internal code). I can't speak to any specific timelines, but there is a lot of work still left to do.

@JohnGalt1717
Copy link

I see the challenges listed, do you know the status on this feature?

We are actively working on the implementations now (it was on hold for a while for patterns/records and while we were finishing up the null safety migration of Google internal code). I can't speak to any specific timelines, but there is a lot of work still left to do.

While you're working on this please consider the copyWith issue carefully. The only solution that actually handles all cases right now is to use nullable functions. Every other solution breaks in some form or another with most tools ignoring that problem and not setting nullable values properly.

Ideally this would use macros (source generators by another name), but would also add a new language construct like C# with the "with" syntax.

If you could do this:

final newContact = contact with {
Property1 = NewValue
};

You'd have an elegant solution to the copyWith problem that I see as the core language issue with implementing this correctly and fully (without resorting to nullable functions)

@jakemac53
Copy link
Contributor

I am definitely aware of the copyWith issue - I am not specifically tackling this as a part of macros because it is really a separate language feature (which macros would want to take advantage of). There are some existing proposals out there for solving that issue I think though.

@insinfo
Copy link

insinfo commented Mar 20, 2024

@josiahsrc What I learned in the 7 years working with Dart in many large projects is that the best solution is to use manual serialization implementing toMap and fromMap where needed, until you have macros or data classes in Dart with serialization, I currently don't use codegen for anything so as not to disturb my workflow, if there is one thing that would help my workflow a lot, it is if there was a way to have abstract static methods in Dart

@tatumizer
Copy link

tatumizer commented Mar 21, 2024

@insinfo: instead of tedious manual implementation, you can write a simple markdown file in an easy-to-parse format, and generate the complete class definition within milliseconds. As a bonus, you have a readable documentation. Here's an example in the simplest format (you can add more columns to it). If you need more methods, you can add them in a separate section. You can generate the entire class definition (toJSON, fromJSON, whatever, tests, invocation of tests etc). It won't be difficult to come up with a general markdown format that covers all functionality currently implemented by other generators.

data class Person

name type required
firstName String *
lastName String *
age int *

Methods

String toString() =>"Person: $firstName $lastName, age $age";

@kaljitism
Copy link

kaljitism commented Apr 24, 2024

@josiahsrc What I learned in the 7 years working with Dart in many large projects is that the best solution is to use manual serialization implementing toMap and fromMap where needed, until you have macros or data classes in Dart with serialization, I currently don't use codegen for anything so as not to disturb my workflow, if there is one thing that would help my workflow a lot, it is if there was a way to have abstract static methods in Dart

I prefer the same approach. I write all my model classes with Copilot and sometimes the file size reaches 3000+ lines of code just boilerplate of the model classes. I too find code gen disturbing my workflow.

Did you try macros for generating models?

@felangel
Copy link

felangel commented May 17, 2024

I've been experimenting with macros recently in case anyone wants to check it out ↓

demo.mp4

Repository: https://github.com/felangel/data_class

*Edit: renamed the annotation to @Data() to avoid any confusion

@tatumizer
Copy link

Nitpick: copyWith won't work if any field has a nullable type.

@jakemac53
Copy link
Contributor

Nitpick: copyWith won't work if any field has a nullable type.

This is a more general issue and we are working on that too :)

@felangel
Copy link

Nitpick: copyWith won't work if any field has a nullable type.

Working on improving that now as well felangel/data_class#12

@kevmoo
Copy link
Member

kevmoo commented May 17, 2024

Nitpick: copyWith won't work if any field has a nullable type.

This is a more general issue and we are working on that too :)

Can you link the issue here? I love the suggested fix!

@JohnGalt1717
Copy link

The only way to do this right is with every copywith property generated being a function.

If the function is null, don't change. If the function isn't null, you do a change and the result could be null or not if the field is nullable.

@scheglov
Copy link
Contributor

Or use records.

class A {
  final int foo;
  final String? bar;

  A({
    required this.foo,
    required this.bar,
  });

  A copyWith({
    int? foo,
    (String?,)? bar,
  }) {
    return A(
      foo: foo ?? this.foo,
      bar: bar != null ? bar.$1 : this.bar,
    );
  }
}

main() {
  final a = A(foo: 0, bar: 'a');
  print(a.bar);

  final b1 = a.copyWith(foo: 1);
  print(b1.bar);

  final b2 = a.copyWith(foo: 1, bar: ('b2',));
  print(b2.bar);

  final b3 = a.copyWith(foo: 1, bar: (null,));
  print(b3.bar);
}

This prints

a
a
b2
null

@JohnGalt1717
Copy link

That's way more gross than the function

@jakemac53
Copy link
Contributor

jakemac53 commented May 17, 2024

Can you link the issue here? I love the suggested fix!

#137 is probably the best one for the overall problem, with the linked #140 being imo one of the more general purpose solutions (allow non-const default values). Then copyWith becomes:

Thingy copyWith({int? myField = this.myField, ...}) => Thingy(myField: myField, ...);

You probably also want to layer on optionally passed parameters + ability to check if a parameter was passed, so that wrapping functions with default values is trivial.

@tatumizer
Copy link

Thingy copyWith({int? myField = this.myField, ...}) => Thingy(myField: myField, ...);

This would work for copyWith, and has a nice property that the nullability of the parameter can match the nullability of the field. (In other proposed solutions, you would have to massage the types)

class Thingy {
  int myField; // not nullable!
  //...
  Thingy copyWith({int myField = this.myField, ...}) => Thingy(myField: myField, ...); // this.myField is not nullable
}

But the solution is not very general either: it won't work for forwarding, which is unlikely to be the only remaining cockroach.
The ability to use non-constant default values is a good feature in its own right though, so it's not a criticism :-)

@jakemac53
Copy link
Contributor

I agree for sure that you want the ability to conditionally pass parameters as well as check if a parameter was passed at all, for a more full fledged solution that allows wrapping of copyWith methods to work well, etc.

@nullbtb
Copy link

nullbtb commented May 17, 2024

I think using a generic wrapper with a very short name ( maybe With ) is cleaner than wrapping through a function or record.

void main() {
  final p = Person(name: 'Peter');
  final d = p.copyWith(age: With(34));
  
  print(d); // Person(name: Peter, age: 34)
  
  print(d.copyWith(age: With(null))); // Person(name: Peter, age: null)
}

class With<T> {
  final T value;
  const With(this.value);
}

class Person {
  const Person({this.name, this.age});
  final String? name;
  final int? age;
  
  Person copyWith({
    With<String?>? name,
    With<int?>? age,
  }) {
    return Person(
      name: name == null ? this.name : name.value,
      age: age == null ? this.age : age.value,
    );
  }
  
  String toString() {
    return 'Person(name: $name, age: $age)';
  }
}

@rrousselGit
Copy link

Note

Initial macro support has landed in Freezed 🥳

It currently supports copyWith/hashCode/toString & co.

@Freezed()
class Person {
  Person(
    String name, {
    required int age,
  });
}

void main() {
  var person = Person('John', age: 30);
  person = person.copyWith(age: 31);

  print(person.name); // John
  print(person.age); // 31
}

Some things are missing because macros aren't stable yet. But it's a start~
See https://github.com/rrousselGit/freezed/tree/macros

@felangel
Copy link

FYI just added support for the simpler copyWith syntax while supporting setting nullable fields to null 🎉

copyWith-demo.mp4

Repo: https://github.com/felangel/data_class

@eernstg eernstg added the brevity A feature whose purpose is to enable concise syntax, typically expressible already in a longer form label Oct 30, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
brevity A feature whose purpose is to enable concise syntax, typically expressible already in a longer form data-classes request Requests to resolve a particular developer problem
Projects
None yet
Development

No branches or pull requests