Skip to content

Add data classes #314

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

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

Add data classes #314

ranquild opened this issue Oct 31, 2017 · 336 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.

@TekExplorer
Copy link

Defining a data class via a primary constructor has literally no practical downside. Defining a data class via its fields only requires the language to make an opinionated choice on how the constructor behaves (positional? named? how are defaults handled? required or nullable?).

I think this choice is a no brainer (:

right! there's literally no way to define defaults with a field-first approach.
plus, if you wanted a Point(x, y) vs a User(name: 'Jack', email: '[email protected]') how would you do that? you cant. not without providing extra information via an annotation or something, but we dont like having annotations directly impact code, and we already have that in the form of constructors, so it makes more sense to do:

data class Point(int x, int y);
data class User({
  required String name,
  required String email,
});

at which point the only "opinion" is in making them final or not, which the answer is obviously yes, because otherwise why would it be a data class?

@ghost
Copy link

ghost commented Feb 17, 2025

Why? Widgets are supposed to be immutable in the first place.

They are. That's why declaring them as value classes makes total sense. But they must not override ==/hashCode.
You will need a lot of explaining as to why widgets could not be declared as value classes, and you will end up with a zoo of unnecessary definitions.

I think this choice is a no brainer

It's a chicken-and-egg kind of problem, which is never easy to resolve. One needs to come up with the list of pros and cons for each variant, assign weights, consider statistics of usage, etc.. BIG brainer. :-)
There's an option to allow both variants though: because dart already supports fields-first approach in existing code.
The feature would make it possible to just erase the declaration of the constructor - it will be generated anyway.

@lucavenir
Copy link

Well I guess the dart team already has an answer about primary constructors and its syntax here.

@lucavenir
Copy link

at which point the only "opinion" is in making them final or not, which the answer is obviously yes, because otherwise why would it be a data class?

immutability and data classes might be orthogonal concepts AFAIK.
but this can be easily fixed via adding a final into the primary constructor as it's valid syntax (again, AFAIK).

@lucavenir
Copy link

Also I wanted to address the following; I might not be on the same page as you guys.

In short: no annotations, no code generation, no macros, just simple prefix data.
Is that possible? [...]

AFAIK, no.
If it was, this issue wouldn't be delayed after macros, years ago, wouldn't it? 😄

All the code snippets shared from @orestesgaolin comment (and below, from other folks) are (imho, heavily) opinionated. And I feel this is bad.

My 2 cents: we need a good amount of annotations (or an ad-hoc syntax) to handle different needs; also the "just ship something that works for me, and let others make do" argument can't work. Like that, you'd let down a lot of people. Keep in mind that this language is trying to find its space in non-client-side software, meaning it has to look "appetizing" to new folks.

Let me summarize the design choices one can make when writing a data class:

  • how should it serialize?
  • is it immutable or not?
  • does it offer handy copy-with methods?
  • how does it compare to others?
  • do we need to override its hash-code or not?
  • etc.

I bet we could collect O(2^N) different answers 😄

To have a glimpse on how deep the serialization topic alone can be, I suggest taking a look at @schultek RFC proposal; spoilers: it is not just about toJson or fromJson.

@pedromassango
Copy link

@kevmoo should we open a discussion for this instead? It would reduce the amount of notifications subscribers are getting

@TekExplorer
Copy link

Why? Widgets are supposed to be immutable in the first place.

They are. That's why declaring them as value classes makes total sense. But they must not override ==/hashCode.

Why? I don't see any reason not to.

@ghost
Copy link

ghost commented Feb 17, 2025

Why? I don't see any reason not to

You don't, but the flutter people do! :-)
I can't find an exact quote, but the idea is this: while deciding whether to replace an old widget with the new one in the tree, the framework, among other things, compares them via ==. And it was experimentally discovered that the override of == may make matters worse, especially if done without a complete understanding of the framework logic.

@AlexanderFarkas
Copy link

@tatumizer Then why define widget as a dataclass?

@ghost
Copy link

ghost commented Feb 17, 2025

Not a dataclass, but a value class.
Because intuitively, it is. The notions have to be defined axiomatically, so that there's a definition somewhere: we call the class a value class iff it satisfies the following properties: (see axioms 1-3 above). The languages are not being designed the way you think they are (have you tried?). You can't say: the class is called a value class if it overrides hashCode/==. 😄
(Please continue downvoting, I really enjoy it ! 😄)


If you need an example of how it really works (in dart and elsewhere), consider a mixin with the on clause:

mixin MusicalPerformer on Musician {
  performerMethod() {
    print('Performing music!');
    super.musicianMethod();
  }
}

The meaning of this declaration is: if your class implements Musician, you can request additional functionality by using a with clause. It works like a contract: the programmer guarantees that the class implements Musician, then the compiler can add a method (but not automatically! you have to ask for it!)

The feature that adds the ==/hashCode/etc... is based on the same principle: the programmer provides an implementation that satisfies certain properties, and then compler can provide some methods. In theory (at least logically), we don't need value classes or data classes - we could just say class A with HashCoder, Stringer, but the conditions of applicability might be more complex than compliance with a certain interface. And sometimes you might want to simply declare: "this class A satisfies certain properties" - this can be valuable for both the reader and the compiler. The same with implementing the interface "Musician": you may implement it with no intention to use any mixins.
Hence value class.

@lrhn
Copy link
Member

lrhn commented Feb 18, 2025

If overriding == makes things worse, it should probably use identical instead.

More to the point, value classes could easily get the same identity behavior as records, meaning that they need not have a persistent identity. That would make them ineligible for being a widget unless they override ==. Otherwise the change detection could detect changes where none happened.
(You can't really lose identity without a state based override of ==, otherwise == won't be reflexive.)

@ghost
Copy link

ghost commented Feb 18, 2025

There are 2 potential explanations for this:

  1. They used == from the start, and by the time it dawned on them that the trick makes things worse, it was too late (b/c backwards compatibility).

  2. they had a supernatural foresight about the future where dart introduces the feature requiring the override of ==. That's why they don't prohibit == - just advise against using it mindlessly (apparently, the trend of overriding == became an epidemic at some point).

It's a pure speculation on my part - I might have misunderstood something slightly or even totally 😄
Having said that, do you agree that overriding == automatically is even worse than doing so mindlessly? It just can't get more mindless than that! 😄

@rrousselGit
Copy link

Widget.operator== is marked with @nonVirtual anyway ; which warns if you override ==.
So switching from == to identical should be pretty harmless.

Regardless, if we get value classes, it makes total sense for Flutter to make a breaking change and adapt. It wouldn't really impact people anyway.

@ghost
Copy link

ghost commented Feb 18, 2025

It might become a breaking change for anyone currently (naively) using an object as a key to a map. When value classes are introduced, everyone will start changing their de facto value classes to the formal value classes. Who knows what can break.

There are 2 cases to be considered:

  1. the object is never used as a key, and is never compared with another. This is 99.9% of classes. The compiler will work hard to generate the methods, and then work even harder to tree-shake them (the success is not guaranteed). Lots of hassle for no profit. Other than that, almost no harm, just a futile activity.

  2. The objects are used as keys. In these 0.1% of cases, all hell will break loose. If the class contains 20 fields, generated ==/hashCode will become very slow. There is no way to memoize hashCode. Somebody can use the object as a key to IdentityMap - it will stop working b/c no identity. Generated == will very often be incorrect b/c the components can be normal objects, not value objects. And probably much more.

I'm trying to recall how many times I used non-trivial objects as keys. Probably twice in my entire life. But in these cases. I won't be able to use generated methods anyway - I didn't need all fields, just some of them.

Don't get me right wrong - I'm not against this ridiculous misguided idea, just wanted to raise some concerns. 😄

@TekExplorer
Copy link

It might become a breaking change for anyone currently (naively) using an object as a key to a map. When value classes are introduced, everyone will start changing their de facto value classes to the formal value classes. Who knows what can break.

There are 2 cases to be considered:

  1. the object is never used as a key, and is never compared with another. This is 99.9% of classes. The compiler will work hard to generate the methods, and then work even harder to tree-shake them (the success is not guaranteed). Lots of hassle for no profit. Other than that, almost no harm, just a futile activity.

  2. The objects are used as keys. In these 0.1% of cases, all hell will break loose. If the class contains 20 fields, generated ==/hashCode will become very slow. There is no way to memoize hashCode. Somebody can use the object as a key to IdentityMap - it will stop working b/c no identity. Generated == will very often be incorrect b/c the components can be normal objects, not value objects. And probably much more.

I'm trying to recall how many times I used non-trivial objects as keys. Probably twice in my entire life. But in these cases. I won't be able to use generated methods anyway - I didn't need all fields, just some of them.

Don't get me right wrong - I'm not against this ridiculous misguided idea, just wanted to raise some concerns. 😄

You're not really making much sense here. Most people switching to value classes would do so for classes that already override == and hashcode. If they do so for a class that did not, then perhaps that's a breaking change, and the usual treatment applies.

We still have language and package versioning, so if you're natively using an object as a map key that suddenly has == overridden, you'd probably notice.

@ghost
Copy link

ghost commented Feb 18, 2025

Most people switching to value classes would do so for classes that already override == and hashcode

How many classes like this are there? I asked chatGPT to list the classes in flutter that override ==/hashCode. There's just a handful of them (like Border, EdgeInsets, Key, Size, Color, etc). I went to API docs to see how these methods are implemented - turns out, automatic generation could handle only half of them. Some use private members, some refer to external code... So, out of thousands of classes in flutter, the feature may work just for 10 simplest instances.

Widget.operator== is marked with @nonvirtual anyway ; which warns if you override ==.
So switching from == to identical should be pretty harmless.

Maybe. But then, the generated ==/hashCode will be unused, so why these methods are even there?

The whole debate is about whether in the rare cases when the user wants to generate ==/hashCode it would be too taxing for said user to write an explicit annotation requesting the feature? And because of that, we have to shove hashCode/== down everybody else's throat no matter if they want it or not (they don't)?
I can't make sense of this story, sorry.

@bernaferrari
Copy link

bernaferrari commented Feb 18, 2025 via email

@zigzag312
Copy link

@tatumizer It's the other way around. Flutter widgets are a rather rare case where classes with a purpose of holding data (config data) shouldn't implement value equality. And even here it's only because the framework wrongly uses == where it should really use identical in order to be explicit that it wants to do a reference equality check.

99% of the time I want to have value equality for data classes. It just makes more sense than reference equality.

Many languages do it like this. For example: Kotlin (data class), C# (record & record struct), Java (record) and Python (@dataclass). Are there frequent complaints how equality works for data classes in these languages?

@ghost
Copy link

ghost commented Feb 18, 2025

In Kotlin, people don't complain because the concept of data class is not well-defined. Here's the thread

I especially like this comment (speaking on the use case of data class)
"Usually that depends on the intended behaviour of equals, whether I want to compare references or the data inside"

Is that all? Why not annotate the class with @auto(.Equals, .ToString). Available on any class. Without theological debate about the nature of "data class".

C# (record & record struct), Java (record) ...

But dart already has record types, exactly with the behavior you expect from data classes. Why do you want to duplicate the concept under a different name?

@cc-rock
Copy link

cc-rock commented Feb 18, 2025

But dart already has record types, exactly with the behavior you expect from data classes. Why do you want to duplicate the concept under a different name?

Because record types cannot be used as data classes.. they are anonymous. A data class is supposed to be a proper named type, and not to be interchangeable with any other data class that contains the same number of properties with the same types in the same order.

Infact, if Dart would allow to give a name to a record type (but a real type name, not just an alias like typedef ) , then we would almost have data classes. Only copyWith would be missing but seems to be coming for records.

@zigzag312
Copy link

In Kotlin, people don't complain because the concept of data class is not well-defined. Here's the thread

So, you think they also wouldn't complain, if Dart wouldn't implement ==/hashCode for data classes? I would.

Why not annotate the class with @auto(.Equals, .ToString)

Because:

  1. Dart is not Rust. Dart doesn't give us fine control over memory or even types like uint64. Why force so much boilerplate in this case?
  2. More frequently we want multiple generated methods. If anything, it would make more sense to do exclusion e.g. @no(.Equals), if you really want such fine grained control, as that would be needed less often.

But dart already has record types, exactly with the behavior you expect from data classes. Why do you want to duplicate the concept under a different name?

No, Dart only has anonymous records. They are more similar to C#'s tuples than to C#'s records. You are comparing just names instead of implementations. Ironically, that is somewhat similar to reference vs value equality issue ;)

Is there any good reason you wouldn't want to have value equality by default for data classes? Since, that would make Dart's data classes different than similar constructs in other languages, the reason would have to be really good.

@bernaferrari
Copy link

So, you think they also wouldn't complain, if Dart wouldn't implement ==/hashCode for data classes? I would

I'm saying exactly that Dart should implement that instead of making value class.

@josiahsrc
Copy link

@lucavenir's comment swayed my viewpoint on this topic.

There's a lot of opinions in this thread, and each has valid points. But it's not going to be possible for us all to agree here on any one thing. Data classes won't satisfy everyone's needs, but some devs do want them. I think the answer is what's already been said...

  • Primary constructors
  • A better, faster build runner (imagine if build runner was built-in and ran natively, we basically have macros at that point)

With these we still get a good dev experience, we can support any of the opinions stated above because build runner is generalized, and we get data classes if we want them.

@equality
@copyWith
@json
class MyData({ int x, @Json(key: 'z') int y })

The dart team is headed in the right direction.

@bernaferrari
Copy link

and then you can have data class == @equality @copyWith @json class

@TekExplorer
Copy link

sounds like we still want "macros"

that said, introspection is a pain
plus, emitting code is still string-based. Perhaps if code_builder gets around to supporting augmentations, and string templates become a thing, both builder+template preferences would be covered. (and perhaps someone will give linting/analysis to that template)

@ghost
Copy link

ghost commented Feb 21, 2025

In Swift, there're some standard protocols (swift's equivalent of interfaces) with the option of autogeneration: Equatable, Comparable, etc.
When the class says it implements Equitable, the compiler can add the generated implementation (unless the user writes it explicitly).
In dart, where the methods ==/hashCode are inherited, this trick won't work quite nicely. Saying "implements Equitable" would be just a statement of the fact. What dart can do instead is:

class A overrides Equitable, Stringable {...}

There can be a standard DataClass interface1 that combines several interfaces.
So instead of writing data class A you would write class A overrides DataClass or something to that effect.
The expression"overrides SomeInterface" is valuable in its own right, irrespective of data classes - it tells us something important and non-obvious about the implementation.

Footnotes

  1. More appropriate name for the interface would be DataClassBecauseIAmTooLazyToTypeEquatableStringable

@TekExplorer
Copy link

TekExplorer commented Feb 22, 2025

In Swift, there're some standard protocols (swift's equivalent of interfaces) with the option of autogeneration: Equatable, Comparable, etc.
When the class says it implements Equitable, the compiler can add the generated implementation (unless the user writes it explicitly).
In dart, where the methods ==/hashCode are inherited, this trick won't work quite nicely. Saying "implements Equitable" would be just a statement of the fact. What dart can do instead is:

class A overrides Equitable, Stringable {...}

There can be a standard DataClass interface[^1] that combines several interfaces.
So instead of writing data class A you would write class A overrides DataClass or something to that effect.
The expression"overrides SomeInterface" is valuable in its own right, irrespective of data classes - it tells us something important and non-obvious about the implementation.

This just looks like it should be a mixin that looks like this:

mixin Equatable {
  external operator ==(Object other);
  external int get hashCode;
}
mixin Stringable {
  external String toString();
}

Which would be filled in by the compiler because that's exactly what external is.

@ghost
Copy link

ghost commented Feb 22, 2025

Interesting. This idea probably has been considered before and rejected. I wonder why.

@lrhn
Copy link
Member

lrhn commented Feb 23, 2025

But sure that has been considered explicitly.
The language semantics of mixing application would be to have the same function applied everywhere, so a behavior where each application gets its own implementation isn't really following language semantics. If the implementation simply claims to be reflection based, then it could be the same function that behaves depending on the runtime type of the object ... but then it should use the runtime type, and not the class where the mixin was applied. So it should somehow record all the classes that the mixin is applied to at compile time and then decide what one it is at runtime. (And if you apply the same mixin more than once, that gets non-trivial.)
So not a good match for the language semantics of mixing application, but possible as a special marker recognized by compilers, and specialized per mixin application.

It's also not a feature that can easily be generalized and made available to users. Which means that only the platform can define these special mixins, and it has to get them right, because they'll be hard to change. (Can still be declared in a package and be versioned, the compiler just needs to recognize all the supported versions. And they'll likely be used so much that a major version increment becomes impossible.)

Would Flutter introduce their own magical interfaces?

@ghost
Copy link

ghost commented Feb 23, 2025

For each of these arguments, one can easily come up with a counter-argument. (Actually, you provided some of the counter-arguments yourself).

And they'll likely be used so much that a major version increment becomes impossible.

How are you going to implement data classes then? They are supposed to support the same functionality as the (more selective) mixins in question. The same applies to any feature generated by (using the term loosely) metaprogramming. E.g. if someone provides a generator of toJson (annotation-based), it faces the same problems as jackson, with the same solutions (that is. adding another annotation per week). What is the difference?

The language semantics of mixing application would be to have the same function applied everywhere, so a behavior where each application gets its own implementation isn't really following language semantics.

These rules can be modified with no harm to anybody. On the surface, the code added to different classes by a mixin even looks identical (external String toString()). The whole argument is based on hair-splitting IMO.

(Further, I would even go as as far as suggesting that the generated toString produces the basic Json format - b/c it's easier than inventing yet another format, and matches JSON.stringify in javascript, which everyone uses to their great satisfaction).

(@bivens-dev: if you need any help, please don't hesitate to write to me personally. My email is in my github account).

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