dart-lang / language

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

Add data classes #314

Open ranquild opened 6 years ago

ranquild commented 6 years ago

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 commented 4 years ago

One interesting thing .. is the possibility to have a cache of the serial version of the instance

I'd wish to make sure that instances of value classes do not have identity, that is, a compiler and runtime is allowed to copy them freely (e.g., in order to use inline allocation in an activation record), and it would also be allowed to reuse an existing instance (the extreme case being that the instances are canonicalized, but we can also have a mixture). It is possible to detect via identical whether two expressions evaluate to the same instance of a value class or they evaluate to different instances with the same contents. But == won't see the difference, and it's basically a bug to rely on identical because we do not promise anything specific about object identity for these instances. This adds further support to your idea about caching instances of value classes: They're supposed to allow ignoring object identity.

DevNico commented 3 years ago

NNBD seems to be almost done is there any timeline on this available yet?

eernstg commented 3 years ago

Not yet, and we don't even have a decision to add value classes. But more than 450 thumbs up is certainly important for the development. Right now, null safety is being rolled out, and it will take a while before it is enabled by default in a stable release.

mnordine commented 3 years ago

Thx @eernstg, is there any way to try @dataClass from a dev build or branch? I tried it the other day, but it didn't seem to work (static error).

eernstg commented 3 years ago

There is some experimental code using @dataClass, but it is not ready for practical applications. It was used as a use case to explore an improvement of the support for kernel transformations, and there's no guarantee that a value class feature will be a direct completion of this approach.

mnordine commented 3 years ago

Thx, I realize it's experimental. Just wondering if there was some way to try it.

easazade commented 3 years ago

@rrousselGit @eernstg @pedromassango has there been any decisions on the next phase of dart feature updates? is there any document available so we can see the timeline for the next feature updates?

eernstg commented 3 years ago

@easazade, we're working on other features at this point and we haven't decided on anything new here. As an aside, you may be interested in https://github.com/dart-lang/language/issues?q=is%3Aopen+is%3Aissue+label%3Apatterns which is somewhat related.

felix-ht commented 3 years ago

@eernstg

Is there any plan to make patterns serializable, without code generation? (Also with support for stuff like BuiltValueField - wirename)?

bouraine commented 3 years ago

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

Is it still on the radar 3 years later ?

easazade commented 3 years ago

I've been checking this topic for quite long time. I hope it is somewhere on top of the guys, let's add this list.

kevmoo commented 3 years ago

FYI folks: null-safety was a 🐻 – (a bear, something very hard, to be clear) – we're wrapping that up now. Thanks for your patience.

tjarvstrand commented 3 years ago

FYI folks: null-safety was a bear – (a bear, something very hard, to be clear) – we're wrapping that up now. Thanks for your patience.

Great! Adding data classes will be a walk in the park then :wink:

==> Why the thumbs down ?

@cedvdb I imagine because many people (me included) would like this to be a compile time feature for performance and type safety.

wliumelb commented 3 years ago

If there was some sort of reflection without builder this wouldn't be needed at all, would it ? You could just extend a class that would reflect on itself to see generate a toString, copy, etc..

Imo if there is runtime reflection this is not needed. The issue is that we need runtime reflection though.

==> Why the thumbs down ?

https://github.com/flutter/flutter/issues/1150

Flutter does not allow reflection because it will increase package size.

insinfo commented 3 years ago

this would be fantastic if it allowed, the longed-for serialization for JSON natively without the need for manual code generation or reflection in time of execution

Today practically all applications depend on serialization for JSON, a modern language like dart should already have a form of native serialization in the language, being obliged to use manual codegen or typing serialization manually is something very unpleasant

easazade commented 3 years ago

@munificent how can we get an update on the static meta programming features of dart or maybe other solutions dart team is working on to solve the problems mentioned in this thread? should we create another thread since dart classes is not gonna happen as a solution for the problems mentioned here?

and thanks for all the efforts you guys are making. dart has changed a lot since last 2 years.

jennsenr commented 3 years ago

@munificent how can we get an update on the static meta programming features of dart or maybe other solutions dart team is working on to solve the problems mentioned in this thread? should we create another thread since dart classes is not gonna happen as a solution for the problems mentioned here?

and thanks for all the efforts you guys are making. dart has changed a lot since last 2 years.

I think that, the static metaprogramming adds many more possibilities, the data class should also be added to the language, although these below end up using the static metaprogramming.

Every time we worry about not contaminating our domain layer with external dependencies, which would make it necessary to implement the data classes every time we start a new project. This is a necessity in the language, it has also been the most requested feature in the community for years, why not listen to it?

mateusfccp commented 3 years ago

@jennsenr If the Dart team chooses to implement data classes in terms of static metaprogramming, they will still put it into the standard library, as far as I am concerned. So, you won't have to depend on external dependencies, neither implement by yourself anyway.

Levi-Lesches commented 3 years ago

There is an issue for metaprogramming at #1482, and I proposed one possible syntax at #1507. The conversation for both of these got a bit long, but the details themselves should be in the top comment.

cedvdb commented 3 years ago

The thing is that meta programming seems to be a big feature that needs careful planning. While data class is more restricted in scope. I'm still wondering if the Record proposal could solve this issue, ie if a record could be used as Data class ?

munificent commented 3 years ago

The thing is that meta programming seems to be a big feature that needs careful planning.

That's true, but it also has the potential to cover data classes as well as many other cases (like JSON serialization, mentioned here). So it may be a net positive in terms of language team efficiency to do metaprogramming and not a slew of smaller one-off features like data classes.

brucexia commented 3 years ago

This is a huge barrier for us looking at flutter as a cross platform option. I have data class extensively in my apps there's no way I can tolerate a language without a similiar feature.

Levi-Lesches commented 3 years ago

@brucexia, data classes are already possible as just regular classes, this proposal just adds some shortcuts:

gmpassos commented 3 years ago

... and JSON codec.

Levi-Lesches commented 3 years ago

Right, edited to fix.

If it helps the discussion I can draw attention to #1565, my second take on code-gen/static metaprogramming that is a lot better than my first one and directly handles data classes as one of its most common use-cases. I implemented all of the above as examples in that issue.

gmpassos commented 3 years ago

My proposal for Data Classes:

data Product {
  String id; // will be required
  String name = 'Unknown';
  bool favorite = false ;
  String? extra ; // nullable, optional, without a default value.
}

var a = Product{ id: '1', name: 'test', favorite: true };
var b = Product{ id: '2' }; // with the default values
var c = Product{ id: '3' , extra: 'foo' }; // defining the optional value.

var keys = a.getKeys(); // list of fields names.

var name = a['name']; // A filed should be accessible dynamically, to facilitate codes that can work in any kind of data class.
var name = a.name; // or can be accessed as a normal field.

var json = a.toJson();

var d = Product.fromJson(json);

extension ProductExtension on Product {
  bool get isUnknown => name == 'Unknown' ;
}
miszmaniac commented 3 years ago

@brucexia, data classes are already possible as just regular classes, this proposal just adds some shortcuts:

  • implicit final, which is just one keyword (and not always desired)
  • implicit ==, which is often overriden anyway to specify which fields are relevant (or for deep equality)
  • implicit hashCode, which would be nice but hardly a show-stopper. I often use the hashCode of a unique field, like email
  • implicit toString, but people might have different preferences. I personally wouldn't like the proposed definition
  • implicit copyWith, but there are some problems with a copyWith that has nullable parameters
  • EDIT: implicit toJson (and maybe fromJson?)

This is exactly what Data class is used for (comparing to Kotlin off course). I'm not sure about fromJson, toJson methods as those conversions are not language features. What if inside of that Data class you'd like to store object that is not serializable to json? Plus those conversions ofter require specific parameters naming inside json, or more complex logic. It would be nice to have better auto serializer to json. But I think it should not be mixed with Data class concept.

ykmnkmi commented 3 years ago

Also allow mutable data classes, or able to set mutable fields.

felix-ht commented 3 years ago

Also allow mutable data classes, or able to set mutable fields.

No. The whole point of DataClasses is that they are immutable. If one attribute is mutable, the whole class becomes mutable.

If you want a partially mutable class just use class, and set the attributes you want immutable to final.

pedromassango commented 3 years ago

Also allow mutable data classes, or able to set mutable fields.

You probably meant a built-in copy/copyWith method which returns a totally new object.

ykmnkmi commented 3 years ago

It's not hard to make mutable modifier

DevNico commented 3 years ago

I feel like a built-in copy / copyWith is already something we would expect with data classes. Indirect mutability would therefore be possible by just creating a new immutable instance with the changes you want. Allowing data classes to have mutable fields would just overcomplicate things without bringing any actual benefits to the table.

Levi-Lesches commented 3 years ago

It seems we can all agree that "data class" just means "a regular class with some pre-written methods, like copyWith, ==, etc.". I don't understand all the talk about mutable fields. Maybe it doesn't fit your design pattern, but that doesn't mean it's invalid, and it certainly doesn't "complicate" things -- all fields are currently mutable by default, and you can use final to indicate immutability. I don't think Dart should be adding in more restrictions when we're asking for more conveniences.

gmpassos commented 3 years ago

I liked the idea to explicitly define if you want a final (immutable) data class. Here's an example using final to define an immutable data class:

final data Product {
  String id; // will be required
  String name = 'Unknown';
  bool favorite = false ;
  String? extra ; // nullable, optional, without a default value.
}
Levi-Lesches commented 3 years ago

I liked the idea to explicitly define if you want a final (immutable) data class.

For those who want this feature today, you can use the @immutable annotation from package:meta. All Flutter StatelessWidgets use it. You'll get a warning if you try adding non-final fields to an @immutable class (or any of its subclasses).

(Granted, the annotation doesn't do anything snazzy like automatic copyWith, ==, etc, which is what this issue is about).

JohnGalt1717 commented 3 years ago

Personally, I'd like exactly the same functionality and syntax as records in C# done with compile time reflection that was fully implemented at the same time so that things like a dart version of Entity Framework was possible as a result. (this would also eliminate most needs for mirrors and in fact most of the use of build_runner too.)

Levi-Lesches commented 3 years ago

... with compile time reflection ...

Then you might be interested in static metaprogramming. Compile-time reflection doesn't currently exist, but static metaprogramming needs it to, so it's being taken as a given there (which is why I pointed out the similarities earlier in this thread).

In general, static metaprogramming (and code-gen) is such a general, broad concept that it will introduce many other more specific features along the way and will enable loads more to be added. That's why it's a useful topic to follow, even if you don't plan on using it directly.

JohnGalt1717 commented 3 years ago

@Levi-Lesches For the record C# has compile time reflection available and EF Core that will be released with .net 6 uses it to generate all of the queries and eliminate the reflection to great effect (300-400% improvement in speed, startup times and more). .NET is in the process of doing compile time reflection on basically everything that doesn't use strings for loads etc. including JSON deserialization, protobuf amongst other things.

It would be a HUGE win for Dart to have this functionality for the same reasons.

brunotacca commented 3 years ago

I could be wrong, but this feature would also alleviate some of the rewriting required between layers in some clean architecture approaches, especially in cases where you have a tight relation between data and entities.

For instance, I've seen some Kotlin approaches to clean architecture where it can reduce it drastically, as shown by this answer at stackoverflow. (Not a Kotlin expert myself)

Def a huge win.

munificent commented 3 years ago

It seems we can all agree that "data class" just means "a regular class with some pre-written methods, like copyWith, ==, etc.". I don't understand all the talk about mutable fields. Maybe it doesn't fit your design pattern, but that doesn't mean it's invalid, and it certainly doesn't "complicate" things -- all fields are currently mutable by default, and you can use final to indicate immutability.

This is exactly why I think it's good to push on static metaprogramming and let users express something like data classes at the library level. Because different users have different policies that they want data classes to have. If we just pick one and bake it into the language, we'll pick wrong for some subset of users.

If we can make static metaprogramming expressive enough that users can write their own data class packages whose resulting syntax is as nice as a built in language feature, then users who want different policies can pick a package that does what they want.

rrousselGit commented 3 years ago

If we can make static metaprogramming expressive enough that users can write their own data class packages whose resulting syntax is as nice as a built in language feature, then users who want different policies can pick a package that does what they want.

I don't see the current proposal for static meta programing as a true syntax improvement for code-generation.

We are very limited by the inability to overwrite an element, such as adding/removing parameters to a function or converting a function into a class.

This forces packages to create new objects instead of modify existing ones. Which means users have to manipulate an object with a name different than what they defined

This is a significant pain point.

Levi-Lesches commented 3 years ago

@rrousselGit, this has been discussed extensively at #1482, #1507, and #1565 (and others). Feel free to add your opinion there, but I think most (including myself) are vehemently opposed to allowing metaprogramming to overwrite user code, as that would lead to a LOT of complicated workflows that users can get lost in, both accidentally and deliberately. All the proposals I've seen build off of that, ie, generating complex code that fits nicely with hand-written code, but doesn't modify in any way.

Speaking of, @munificent, can you take a look at #1565? It's my second draft (with #1507 being the first) of a static metaprogramming proposal. It focuses mainly on the end-user experience: writing, applying, and composing macros with only a small discussion on how the compiler will handle the change, since modules will probably handle the latter. I'd appreciate any feedback.

rrousselGit commented 3 years ago

All the proposals I've seen build off of that, ie, generating complex code that fits nicely with hand-written code, but doesn't modify in any way.

I feel that this is the opposite.

Numerous usecases would want to override the definition, including data-classes.

One issue with the current data-classes proposals is, they are defined as:

@data
class Foo {
 final int property;
}

And expect the macro to generate a constructor.

The problem is, this prevents users from defining how the constructor would look like. Some properties may be required, some optional, some named, some positional, or even some with default values. We also have to consider super constructors or whether the constructor allows "const" or not.

That's why Freezed is constructor-first:

@freezed
class Foo with _$Foo {
  const factory Foo({@Default(42) int property}) = _Foo;
}

But this still doesn't support super constructor and has other syntax issues (like the @Default because param = x is not allowed on factory constructors).

So realistically, we would want to write:

@freezed
class Foo extends Base {
  const Foo({int property = 42}): super(...);
}

But this would require overriding the definition (to change the constructor parameters into this.property or to make the constructor a factory)

Arguably we could declare the class as _$Foo and generate Foo, but this causes other issues:

These workarounds once again degrade the user experience to be sub-par what a language feature would offer.

Levi-Lesches commented 3 years ago

Constructors are a massive problem for metaprogramming because they don't have any form of inheritance (so you can't get them by extends or implements), they're not shared with mixins (no with eithers), and extensions can't define constructors.

That's why partial classes (#252) would help. With partial classes, you'd write:

// foo.dart
part "foo.g.dart";

@data
partial class Foo {
 final int property;
}

and the macro would generate:

// foo.g.dart
part of "foo.dart";

partial class Foo {
  Foo(this.property);
}

and you'd be able to use the constructor wherever you import Foo. Of course, partial classes would also be able to split other members apart as well, not just constructors. This way, we keep hand-written and generated code separate, while allowing the compiler to recognize them as the same. (Partial classes also allow one more benefit: the generated code looks as if a human could have written it. Partial classes can be useful for humans too, and once people get used to them, it takes away some of the magic of "where did this constructor come from?")

rrousselGit commented 3 years ago

That's why partial classes (#252) would help. With partial classes, you'd write:

There seem to be a misunderstanding. My comment was arguing against this syntax.
I am aware that the current data-class proposal skips constructors. My point is that skipping constructors is bad.

A macro should not take any assumption about whether a parameter is required/optional, positional/named, and with or without a default value. Nor can a macro know about what super constructor to use or whether const constructors are allowed or not.

Levi-Lesches commented 3 years ago

Oh well if the user wants to manually specify a constructor, they can, and the macro can just ignore it.

@data
partial class Foo {
  final int property;
  Foo({required this.property});  // now the macro won't generate a constructor
}

Or maybe you can have a system based on annotations (like the ones in #1565):

@data 
partial class Foo {
  @defaultField(42)
  final int number;
  final bool condition;
}
// generated:
partial class Foo {
  const Foo({required this.condition, this.number = 42});
}

Or whatever other conventions people want to set. And of course, if you push this to the extreme, you're better off writing your own constructor than using a million annotations. Point is, by allowing users to write their own macros with meaningful reflection, it can be worked out.

rrousselGit commented 3 years ago

Manually writing the constructor or using annotations like @default/@positional is degrading the syntax

If data classes implemented using code generations have a noticeably lower quality than if they were built in the language, then I don't think we can say that we fixed the issue.

Levi-Lesches commented 3 years ago

Fair point, although "degrading the syntax" is largly subjective. What do you think an ideal syntax look like? Then this issue can focus on that, separately from code-gen.

rrousselGit commented 3 years ago

I believe users want something as close to Kotlin as possible. With Dart, that could be:

const class User(final String name, {final int? age});

Or for unions:

class UserState {
  const class Loading([final Data data = null]);

  class Data(final String name, {int? age}) implements Human
    : assert(age == null || age > 0);

  class Error(final Object error);
}

The goal being no duplication, while still having control over named vs positional parameters & co.

The current Freezed syntax being

@freezed
class UserState with _$User {
  const factory UserState.loading([Default(null) Data data]) = Loading;

  @Assert("age == null || age > 0")
  @Implements<Human>()
  factory UserState.data(String name, {int? age}) = Data;

  factory UserState.error(Object error) = Error;
}

Which is passable. But still objectively worse due to default values / asserts / super constructors not being ideal, and some duplicate.

And even then, this syntax is not compatible with the current static meta programming proposals.

VKBobyr commented 3 years ago

Is there any indication whether this is still on "the radar" circa 2017?