dart-lang / language

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

Static extension methods #723

Open rrousselGit opened 4 years ago

rrousselGit commented 4 years ago

Motivation

Currently, extension methods do not support adding static methods/factory constructors. But this is a missed opportunity!

There are many situations where semantically we want a static method/factory, but since the type is defined from an external source, we can't.

For example, we may want to deserialize a String into a Duration.

Ideally, we'd want:

extension ParseDuration on Duration {
  factory parse(String str) => ...;
}

Duration myDuration = Duration.parse('seconds: 0');

But that is currently not supported. Instead, we have to write:

Duration parseDuration(String str) => ...;

Duration myDuration = parseDuration('seconds: 0');

This is not ideal for the same reasons that motivated extension methods. We loose both in discoverability and readability.

Proposal

The idea is to allow static and factory keyword inside extensions.

Factory

Factories would be able to capture the generic type of the extended type, such that we can write:

extension Fibonacci on List<int> {
  factory fibonacci(int depth) {
    return [0, 1, 1, 2, 3, 5];
  }
}

Which means we'd be able to do:

List<int>.fibonacci(6);

But not:

List<String>.fibonacci(6);

Factories would work on functions and typedefs too (especially after #65):

typedef MyEventHandler = void Function(Event event);

extension MyShortcut on MyEventHandler {
  factory debounced(Duration duration, MyEventHandler handler) {
    return (Event event) { .... } 
  }
}

Static members

Using extensions, we would be able to add both static methods and static properties.

They would not have access to the instance variables, nor the generic types. On the other hand, static constants would be allowed.

We could, therefore, extend Flutter's Colors to add custom colors:

extension MyColors on Colors {
  static const Color myBusinessColor = Color(0x012345678);
}

Or Platform to add custom isWhatever:

extension IsChrome on Platform {
  static bool get isChrome => ...;
}

which, again, would work on functions and typedefs

typedef MyEventHandler = void Function(Event event);

extension MyShortcut on MyEventHandler {
  static void somePremadeHandler(Event event) {}
}
esDotDev commented 3 years ago

I know enums were mentioned in passing, but just to make it explicit, another use case where this is sorely needed is enum parsing.

We can make MyEnum.first.value but we can not make MyEnum.fromValue("first"), which feels incomplete / limited.

lrhn commented 3 years ago

We now have generalized type aliases which allow you to call static methods and constructors through an alias. Example:

typedef MyList = List<int>;
main() {
   MyList.filled(4, 4);
}

That means that if extension Foo<T> on List<T> { ... } simply introduced an alias (like typedef Foo<T> = List<T>; would), then we'd get static member and constructor access for free. That would be a minimal "extension type" behavior.

Levi-Lesches commented 3 years ago

Nice, glad to hear that feature made it through!

To clarify, would this mean extensions can:

I believe the first two were originally problematic because once Dart could resolve a function call, it would resolve to the original type, and wouldn't even check for any extensions. Now, by specifying the type, the ambiguity should be resolved.

lrhn commented 3 years ago

If all it does is to introduce an alias, like one you could write yourself, then no, you get nothing new except the ability to call static members. Treating the type specially wrt. resolving extension methods against it would require it to be more than an alias. A type alias goes away the moment you use it, it just becomes the aliased type. So, what you ask for would require more special machinery (which is what the "extension type" request is asking for too).

I just wanted to say that the static members of this request are within (semi-)easy reach.

(You'll probably never get to add fields using static extension members, which is the full name of the "extension methods" feature. If we ever pretend that you can, it'll just be hiding an Expando somewhere, and then it won't work for, e.g., integers and strings).

inkalsee2 commented 3 years ago

@creativecreatorormaybenot

@tatumizer Yes, it does because it would print baz before and extension after your proposed change.

class Foo {
  static bar() => print('baz');
}

extension FooExtension on Foo {
  static bar() => print('extension');
}

Agree. New bechavior should show conflict error. it's breaking changes. Probably we could break it for Dart 3.0? Together with strong null safety)

Agree, should also remove Symbol ? and support covariant/contravariance ..

droplet-js commented 3 years ago

any news update?

batuhankrbb commented 3 years ago

Please add this feature we really want it.

Silentdoer commented 3 years ago

We will introduce the syntax:

extension Name on static DeclName<T> { ... static looksLikeStaticMethod() ... ... factory Name.name() ... // Factory constructor with return type DeclName<T> ... factory Name() ... // Empty-named constructor too. ... static const myConst = constExpr; }

it not consistent with:

extension Name<T> on SomeType<T> {
  ... instanceMethod() ...
}

why not

extension Name<T> on static DeclName<T> {
factory Name.name() {T t ...}
Silentdoer commented 3 years ago

eh, since the following code is ok,

test(){
  StringExt("aaa").bar();  // prints 8
  "aaa".bar();  // prints 8
}
extension StringExt on String {
  void bar(){
    print(8);
  }
}

i think static method should can both invoke by String and StringExt, otherwise it seems like not consistent?

Silentdoer commented 3 years ago

eh, since the following code is ok,

test(){
  StringExt("aaa").bar();  // prints 8
  "aaa".bar();  // prints 8
}
extension StringExt on String {
  void bar(){
    print(8);
  }
}

i think static method can both invoke by String and StringExt, otherwise it seems like not consistent?

Guang1234567 commented 3 years ago

Link the real world case to explain Is necessary to improve dart's extension?

jodinathan commented 3 years ago

Isn't this connected or very useful to meta programming?

lamnguyen2169 commented 3 years ago

I have no idea why this kind of vital feature isn't supported yet.

For instance, I have the DateTime extension as following:

extension DateTimeExtensions on DateTime {
  static DateTime yesterday() {
    return DateTime.now().add(Duration(seconds: -86400));
  }

  static DateTime tomorrow() {
    return DateTime.now().add(Duration(seconds: 86400));
  }
}

The result as I want is that:

final DateTime yesterday = DateTime.yesterday();
final DateTime tomorrow = DateTime.tomorrow();

But in the real world is: final DateTime yesterday = DateTimeExtensions.yesterday();

Don't you think that DateTimeExtensions.yesterday() is quite stupid? It should be DateTime.yesterday() instead.

Please support this feature. It's really necessary.

creativecreatorormaybenot commented 3 years ago

@lamnguyen2169 Why is this necessary? Your example has no functional benefits.

I agree that this is more beautiful in some cases. However, I would suggest that there are more vital features than this.

lamnguyen2169 commented 3 years ago

@creativecreatorormaybenot It is not about the beautify code. It's about the meaning and the context of extension. When I use .yesterday() it should be the meaning that I call that method from DateTime not for DateTimeExtensions extension itself (just exactly the same with DateTime.now()).

The DateTimeExtensions should only be meaning about the extensions of DateTime not the class DateTimeExtensions.

DateTimeExtensions.yesterday() is really stupid. It should be DateTime.yesterday() instead. Extension not only be applied for the instance of the Class. It also comes with some contexts by the Class itself. My case is one of those situations. You can't use date.yesterday() or DateTimeExtensions.yesterday(). It must be DateTime.yesterday().

I am also facing the same static issue in inheritance.

class A {
    static void doSomething() {
        /// Do something here
    }
}

class B extends A {
}

void main() {
    A.doSomething(); // works
    B.doSomething(); // error
}

Anytime I want to call the doSomething() method I must use class A for doing that instead of B.doSomething()

Many of other languages support those features but Dart. Why???

lrhn commented 3 years ago

Static inheritance is a very different feature, and one Dart has deliberately chosen not to support (along with calling static methods on instances, which Java also allows). I don't expect that to change, independently of this feature. Having multiple ways to call the same function, which works the exact same way no matter how you call it, is not a goal. If you want a static function which works on all subclasses of A, likely taking an A value as argument, just make it a top-level function. Possibly even a generic one with <T extends A>(T argument). The original Java which introduced inheritable static functions couldn't do that (no top-level functions at all).

There is nothing preventing Dart from introducing extension static methods, apart from it being a higher priority than the other features we are also adding. You wanting the feature is one more vote towards it being a worthy candidate for spending our finite developer time on, so do remember to add a :+1: on the issue.

Claiming that this feature is essential is not making your position more convincing, though. You could have a top-level yesterday getter (should be a getter, not a function) instead of a static on DateTime, or a previousDay extension instance getter on DateTime which works on every date, not just the implicit DateTime.now(). Sure, having DateTime.yesterday looks nice (and extremely specialized), but there are other ways.

jodinathan commented 3 years ago

IMHO static meta programming, union types and sum types are much more important to the language.
Also https://github.com/dart-lang/language/issues/1855 is a simple syntax sugar but so nice.

lrhn commented 3 years ago

(As a general request: Please try to avoid comparing the relative importance of individual features on the issue tracker for any of the features. Just vote for the features you want with :+1:, and discuss just the feature itself in its own issue. Promoting a particular feature on its own merits is the best way to get it implemented. Comparing its functionality to other features is fine, that's part of the design process. Comparing its urgency or importance to those of another feature isn't relevant to designing or discussing a feature. Everybody has their own priorities, and in the end the Dart team will have to make a decision anyway, based on importance, urgency, impact, implementation cost, available implementation resources and whichever constraints apply for a particular day. Basically: Focus on the ball. Note to self: Yes, that also applies to you!)

lastmeta commented 2 years ago

I also agree that dart should support adding static methods. Ran into this issue today, see https://stackoverflow.com/questions/70598085/extension-method-on-a-class-without-a-constructor-in-dart.

Bottom line is that this is a 'syntax' or 'semantics' issue. I want to be able to say FontWeight.medium but I can't. I have to create my own class like this:

class FontWeightNames {
  static const FontWeight thin = FontWeight.w100;
  static const FontWeight extraLight = FontWeight.w200;
  static const FontWeight light = FontWeight.w300;
  static const FontWeight regular = FontWeight.w400;
  static const FontWeight normal = FontWeight.w400;
  static const FontWeight medium = FontWeight.w500;
  static const FontWeight semiBold = FontWeight.w600;
  static const FontWeight bold = FontWeight.w700;
  static const FontWeight extraBold = FontWeight.w800;
  static const FontWeight thick = FontWeight.w900;
}

then import that and say FontWeightNames.medium

Not as good. Its about namespacing, the 'medium' concept belongs in FontWeight as a static method but I can't add it there.

Namespacing takes a hit, but I can still technically achieve what I want computationally. The code could just look nicer.

How to implement?

I don't know, writing a language is beyond me, but I'll tell you what I expected. I first wrote this:

extension FontWeightExtension on FontWeight {
  static const FontWeight thin = FontWeight.w100;
  static const FontWeight extraLight = FontWeight.w200;
  static const FontWeight light = FontWeight.w300;
  static const FontWeight regular = FontWeight.w400;
  static const FontWeight medium = FontWeight.w500;
  static const FontWeight semiBold = FontWeight.w600;
  static const FontWeight extraBold = FontWeight.w800;
  static const FontWeight thick = FontWeight.w900;
}

and I expected that anything in the {...} gets applied essentially to the FontWeight, not the FontWeightExtension. In the language can we say, "well if this is an extension, suspend the usual static rules and apply them to the class being extended..."

As a user of the language API that's about as detailed as my model gets, it's all magic under the hood to me, but that's how I expected it to behave. Perhaps there's no elegant implementation for that so I would then expect a slight wrinkle on my end, something like:

extension FontWeightExtension on FontWeight {
  extend static const FontWeight thin = FontWeight.w100;
  extend static const FontWeight extraLight = FontWeight.w200;
  extend static const FontWeight light = FontWeight.w300;
  extend static const FontWeight regular = FontWeight.w400;
  extend static const FontWeight medium = FontWeight.w500;
  extend static const FontWeight semiBold = FontWeight.w600;
  extend static const FontWeight extraBold = FontWeight.w800;
  extend static const FontWeight thick = FontWeight.w900;
}

Anyway, those are my thoughts on the issue. Honestly, when in the world are you going to want to add a static to the extension itself?

Mohsen7s commented 2 years ago

After one year of discussion I guess dart team should at least give us a straight answer, either if they will make it happen or not. This future is a must for extra-large projects where we cant spam it with new classes, It also make code cleaner and give us easier human-readable code. Any update on issue ?

munificent commented 2 years ago

Bottom line is that this is a 'syntax' or 'semantics' issue. I want to be able to say FontWeight.medium but I can't. I have to create my own class like this:


class FontWeightNames {
  static const FontWeight thin = FontWeight.w100;
  static const FontWeight extraLight = FontWeight.w200;
  static const FontWeight light = FontWeight.w300;
  static const FontWeight regular = FontWeight.w400;
  static const FontWeight normal = FontWeight.w400;
  static const FontWeight medium = FontWeight.w500;
  static const FontWeight semiBold = FontWeight.w600;
  static const FontWeight bold = FontWeight.w700;
  static const FontWeight extraBold = FontWeight.w800;
  static const FontWeight thick = FontWeight.w900;
}

You don't. You can instead do:

// font_weight.dart
const FontWeight thin = FontWeight.w100;
const FontWeight extraLight = FontWeight.w200;
const FontWeight light = FontWeight.w300;
const FontWeight regular = FontWeight.w400;
const FontWeight normal = FontWeight.w400;
const FontWeight medium = FontWeight.w500;
const FontWeight semiBold = FontWeight.w600;
const FontWeight bold = FontWeight.w700;
const FontWeight extraBold = FontWeight.w800;
const FontWeight thick = FontWeight.w900;

// your_app.dart
import 'font_weight.dart' as font_weight;

main() {
  print(font_weight.normal);
}

Or you can do:

import 'font_weight.dart';

main() {
  print(normal);
}

Dart isn't Java. You aren't obligated to stuff every declaration inside some class. If you want namespaces that are easy to add to, populate, and rename... that's what libraries are. :)

Instead of trying to write Java in Dart and then add features to Dart when we discover that you can't write Java in Dart, we'll probably be better off if we write Dart in Dart.

rrousselGit commented 2 years ago

We're not allowed to augment classes outside of the library that defined them.

That'd be problematic otherwise.

rrousselGit commented 2 years ago

I don't like the usage of "augment" to express an extension here.

And ultimately a problem that needs to be solved with extensions that augment libraries don't have to care about is: name conflict resolution.
It's entirely possible that two different extensions add methods with the same name:

static extension A on String {
  static void doSomething() {}
}

static extension B on String {
  static void doSomething() {}
}

This code is not an error, and you can individually call the method of your choice through:

A.doSomething();
B.doSomething();

We can also do things like:

export 'my_extensions.dart' show A;
jakemac53 commented 2 years ago

Unfortunately library prefixes don't actually solve this problem well in the case of larger packages such as flutter, which is almost certainly why they use classes to wrap up these values.

You wouldn't want to import all of flutter under a prefix, but you do in this case just want only certain identifiers to be under some namespace. The only way to do that is making them static members on a class today.

So I think this feature request is actually quite valid, and it would also allow other nice things like making specialized "constructors" (really static factory methods) on types, etc.

Levi-Lesches commented 2 years ago

@lrhn I read your earlier comments on why we can't allow static members in a regular extension, since it might be generic or have other complications. In other words, if you want this:

extension on A<T> {
  /// This method needs the type argument [T].
  T getValue() => this.value;

  /// But this static member does not.
  String getName() => "A";
}

You instead need to write:

/// Regular extension with a type argument.
extension on A<T> {
  /// This method needs the type argument [T].
  T getValue() => this.value;
}

/// Static extensions cannot have type arguments.
static extension on A { 
  String getName() => "A";
}

I get that, but my problem is that adding static before the word extension is not so obvious. Additionally, when you consider that being forced to move all your static members to another extension means that other extension is by definition compatible with static extension members, then static extension becomes redundant. How about rephrasing the rule instead: It is an error to declare a static member in an extension with an on-type that is incompatible with static members (ie, generics, function types, etc.).

The first and most important benefit to me is that extensions that declare static members with a compatible on-type will just work. The other benefit is clearer, more intuitive errors that only pop up when the user tries to do something they can't, and explains why, instead of a blanket error that appears on all uses of static -- even if it "should" work -- and redirects users to a nearly identical feature that somehow magically does work. Not to mention the confusion when users try making their extensions static extensions only for all their non-static members to start throwing errors!

The downside is that whereas you can currently define static members on the extension itself, this wouldn't allow that. However, while technically a breaking change, I see it as positive because:

To summarize:


class A<T> { }

/// Here, [temp] is unreachable with no error, warning, or lint. 
/// 
/// It should instead be considered declared on [A]. 
extension on A { static int get temp => 42; }

/// Here, [temp] is declared on [B] instead of [A] with no error, warning or lint.
/// 
/// It should instead be considered declared on [A]. 
extension B on A { static int get temp => 42; }

/// Using `C.temp` works, but `C<dynamic>.temp` doesn't compile. No error, warning, or lint. 
/// 
/// [temp]'s declaration should throw an error, as static and generics don't mix.
extension C<T> on A<T> { static int get temp => 42; }

void main() {
  print(A.temp);  // Error: The getter 'temp' isn't defined for the type 'A'.
  print(B.temp);  // works, but shouldn't. See above.
  print(C.temp);  // works, but shouldn't. See below.
  print(C<dynamic>.temp);  // Error: Only a generic type, generic function, generic instance method, or generic constructor can be type instantiated.
}
lrhn commented 2 years ago

@Levi-Lesches Since my first arguments, we have done something similar for type aliases.

Some type aliases allow you to access static members through the alias. Whether they do so depends on the shape of the aliased type. For example typedef IntList = List<int>; does allow you to access static members of List as IntList.staticOnList. It even allows constructors like IntList.filled(4, 0).

Not all type aliases allows that. For example typedef Stupid<T> = T;. You can't do Stupid<List>.staticOnList. We want to know, statically and only based on the type alias declaration which class/mixin it will be aliasing.

We can use the same logic on targets of extensions. An extension MyList<T> on List<T> could "give access" to statics on List, meaning have its own static members appear to be on List, and extension MyGeneralExtension<T> on T won't give any access to static members.

Basically, some type expressions "denotes a class/mixin". If such a type expression is the target of an extension, we make the extension static members available on that target class/mixin. Otherwise we do not.

As usual, there can be conflicts between different extensions on the same target, and for statics, any conflict is likely fatal. We can't use subtyping to pick a "best" choice.

It also means that while extension on A matches any subtype of A for instance members, it only matches A itself for static members. That's fair, it's like both kinds of members were added to A, so instance members are inherited to subclasses and statics are not. (And if a subclass introduces its own instance member with the same name, it shadows the one on A.) All perfectly consistent.

You can still add static members to extensions which do not denote a class/mixin (but probably no constructors, which is otherwise an option), you can just only access them through the extension. You can always access the statics of the extension through the extension (that's the scope override you need in case of conflicts).

DetachHead commented 2 years ago

the docs seem to say this is already supported, but it isn't?

Extensions can also have static fields and static helper methods.

shilangyu commented 2 years ago

the docs seem to say this is already supported, but it isn't?

Extensions can have static fields, but these static fields are not extending the extended type. So

extension A on Duration {
    static const one = Duration(seconds: 1);
}

Will result in being able to use A.one, not Duration.one (which the issue is about).

cedvdb commented 2 years ago

Some type aliases allow you to access static members through the alias. Whether they do so depends on the shape of the aliased type. For example typedef IntList = List<int>; does allow you to access static members of List as IntList.staticOnList. It even allows constructors like IntList.filled(4, 0).

@lrhn This is currently supported ?

I'm trying

typedef DaySet = Set<Day>;

extension on DaySet {
  static const workdays = {
    Day.monday,
    Day.tuesday,
    Day.wednesday,
    Day.thursday,
    Day.friday,
  };
}
eernstg commented 2 years ago

If the extension has no name then there's no way to access its static members from outside the body of the extension itself. But you can do the following:

enum Day { monday, tuesday, wednesday, thursday, friday, saturday, sunday }

typedef DaySet = Set<Day>;

extension DaySetExtension on DaySet {
  static const workdays = {
    Day.monday,
    Day.tuesday,
    Day.wednesday,
    Day.thursday,
    Day.friday,
  };
}

void main() {
  print(DaySetExtension.workdays);
}

If the idea was to access workdays as a static member of Set (cf. the title of this issue) then it won't work, because static members of any declaration are associated with that declaration, they aren't implicitly added to any other declaration.

Also, the very fact that this is an extension on Set<Day> illustrates that the notion of adding static members to a class in an extension is a new concept that would need to be specified and motivated. For instance, would static members added by an extension on Set<Day> be treated as if they were declared in the class Set with the added constraint that the type parameter had the value Day? Would we be able to add a conflicting set of static declarations to Set<Night> or Set<String> or any other parameterized type based on Set? How about Set<Object>, would static members in there need to coexist with static members of Set<T> for all T where T <: Object (so they'd have to have different names)?

I think there's a substantial amount of interest in adding a feature that would allow static members to be added to a class/extension/mixin (similarly to the apparent addition of instance members using an extension declaration)—453 thumbs up right now—but no such feature has been proposed in detail as far as I know, and it is likely to be a separate mechanism, not just a generalization of the affordances offered by extension.

rrousselGit commented 1 year ago

Coming back to this, static extensions would likely be helpful for code-generation-based decoding

Decoding using code-gen relies on a class having a fromJson constructor. But some types don't always have one.
Being able to use extensions to add the missing fromJson could help here

We could support things like:

static extension on DateTime {
  factory DateTime.fromJson(Object obj) {...}
}

extension on DateTime {
  Object toJson() => ...
}

Then, this would allow defining:

@serializable
class Foo {
  DateTime time;
}

And importing the extensions would be enough to set up things correctly.

This could be a non-negligible simplification over approaches like json_serializable's JsonConverter

lukepighetti commented 1 year ago

I agree with the above. But in an ideal world we would be driving towards also have interfaces that define static members, and extensions that force a class to implement these members. For example

interface Decodable<T> {
  factory fromJson(T json);
}

interface Encodable<T> {
  T toJson();
}

typedef JsonMap = Map<String, dynamic>;

extension JsonSerializableVector2 on Vector2 implements Decodable<JsonMap>, Encodable<JsonMap> {
  factory fromJson(JsonMap json) => Vector2(json['x']!, json['y']!);
  JsonMap toJson() => {'x': x, 'y': y};
}

main(){
  final Vector2 vec = jsonDecode<Vector2>(json);
  final Map<String,dynamic> payload = jsonEncode(vec); 
}
Levi-Lesches commented 1 year ago

@lukepighetti see #356 for "abstract static" members:

static abstract class Decodable<T> {
  factory fromJson(T json);
}

abstract class Encodable<T> {
  T toJson();
}

typedef Json = Map<String, dynamic>;

extension JsonVector2 on Vector2 implements Decodable<Json>, Encodable<Json> {
  factory fromJson(Json json) => Vector2(json["x"]!, json["y"]!);
  Json toJson() => {"x": x, "y": y};
}

There's a bit of talk about the exact syntax there but the point is that abstract inheritance should be separate from regular inheritance (otherwise, for example, every Flutter widget would have to implement Widget.canUpdate, which we obviously don't want).

FMorschel commented 1 year ago

Maybe something like a static mixin that only attaches to the single class it has been inserted. Maybe create a lint so at every inherited non-abstract class we would be reminded to place the mixin? So this way we could have the static mixin as the base class.

leddy231 commented 1 year ago

From playing around with records in Dart 3 alpha, this feature would be extremely useful when using records as lightweight data classes.

Something like this would be possible:

typedef User = ({String name, int id});

static extension on User {
  static User? fromJson(Map<String, dynamic> json) {
    if (json case {'name': String name, 'id': int id}) {
      return (name: name, id: id);
    }
    return null;
  }
}
rubenferreira97 commented 1 year ago

For moments I would like to achieve what @leddy231 proposed, but after thinking a bit it isn't "dangerous" to give a extension on a typedef like that? A extension to a shape that is not trully generic seems a bad example. Not every ({String name, int id}) is a User.

Imagine the following example:

//user.dart
typedef User = ({String name, int id});
static extension on User {
  static User? fromJson(Map<String, dynamic> json) {
    if (json case {'name': String username, 'id': int id}) {
      return (name: username, id: id);
    }
    return null;
  }
}

//category.dart
typedef Category = ({String name, int id});
static extension on Category {
  static Category? fromJson(Map<String, dynamic> json) {
    if (json case {'name': String categoryName, 'id': int id}) {
      return (name: categoryName, id: id);
    }
    return null;
  }
}

void main () {
    // I have user.dart and category.dart imported, how would they disambiguate since User = Category?
    // I assume Dart would give a compile error like:
    // "method 'fromJson' is defined in multiple extensions for '({String name, int id})' and neither is more specific."
    final User joseph = (id: 1, name: "Joseph");
    final Category news = (id: 1, name: "News");
    final User danny = User.fromJson({"username" : "Danny", "id": 2 });
    final Category nature = Category.fromJson({"categoryName" : "Nature", "id": 2 });
}

However I think this example would be pratical if applied to dataclasses (when and if they are implemented using records under the hood) or maybe when we treat the extension to the record type itself like:

// But this is what it´s asked on this issue since a `dataclass` should be a class.
dataclass User (String name, int id);
static extension on User {
  static User? fromJson(Map<String, dynamic> json) {
    if (json case {'name': String username, 'id': int id}) {
      return (name: username, id: id);
    }
    return null;
  }
}

// or

// TBH I don't know if anyone would use this
typedef TwoString = (String, String)
static extension on TwoString {
  static TwoString create(String a, String b) => (a, b);
}
rrousselGit commented 1 year ago

@rubenferreira97 I don't think that's an issue.
Static extensions would by default be specific to "User", as you'd have User.something

On the flip side, dataclasses / primary constructors don't have this problem at all, as you could directly define the static members on said dataclass:

@data class User(int age, String name);

vs

@data class User(int age, String name) {
  factory User.fromJson(json) => User(user['age'] as int, user['name'] as String);
}

Kind of like the recent "enum class" syntax.

leddy231 commented 1 year ago

Agree with @rrousselGit , I would argue that a static extension on a typedef would create a namespace with the same name as the typedef and put the static functions there, and not put them on the raw record type.

So using the code from the example:

User.fromJson(...) 
// valid, and only points to the fromJson from the extension on User

Category.fromJson(...) 
// valid, same as above

(String name, int id).fromJson(...) 
//invalid, no extensions defined on the raw record type

you can still do wonky things with typedef records like

Category news = User.fromJson(...)

but thats just how records work, nothing to do with the static extensions

rubenferreira97 commented 1 year ago

Someone correct me if I am wrong, but a typedef does not hold a "Type reference" and is just an alias that the compiler will replace afterwards, so:

typedef User = ({String name, int id});
typedef Category = ({String name, int id});

User.something(); // same as ({String name, int id}).something(); on runtime
Category.something(); // same as ({String name, int id}).something(); on runtime

Both are just an alias and can't mean different things. If you apply a extension to User or Category, you trully are just applying a extension to ({String name, int id}) no? Or am I missing something?

leddy231 commented 1 year ago

@rubenferreira97 I think you are correct, since typdef is an alias it gets replaced at some point during compilation. But in the cases where it is used as a static type / namespace it could be replaced with something else to make the compilation work.

With code like this:

typedef User = ({String name, int id});

User jeff = ...

User.doStaticThing(...)

The compiler could generate a new namespace (lets call it $UserStatic) for User to store static stuff. Then, compile as follows:

({String name, int id}) jeff = ...

$UserStatic.doStaticThing(...)


The static extension would be replaced in the same way
```dart
static extension on User {
  static User? fromJson(...) {
    ...
}

becomes

static extension on $UserStatic {
  static ({String name, int id})? fromJson(...) {
    ...
}
eernstg commented 1 year ago

The danger which is mentioned here by @rubenferreira97 is essentially that a nominal type has an unbreakable association with a specific declaration, and a structural type is considered the same as anything and everything which is structurally identical (and when we take covariant subtyping into account then we need to consider even more types).

However, we can make the choice to associate a record type with a nominal type by using an inline class. Inline classes with distinct declarations (that don't explicitly inherit from each other) are unrelated, and this means that the underlying record types may be the same, but the inline types will be distinct, and assignments across those type differences will be compile-time errors.

(Of course, an inline type is a soft protection because it is possible to use as and is to get to the underlying representation type, but if you want to maintain this typing discipline then the compile-time errors will be there for you as long as you don't use as or is to break it.)

So here's a version using inline classes:

// Library 'user.dart'.

inline class User {
  final ({String name, int id}) value;
  User(this.value);

  String toJson() => '{"name": "${value.name}", "id": ${value.id}}';

  static User? fromJson(Map<String, dynamic> json) {
    if (json case {'name': String username, 'id': int id}) {
      return User((name: username, id: id));
    }
    return null;
  }
}

// Library 'category.dart'.

inline class Category {
  final ({String name, int id}) value;
  Category(this.value);

  String toJson() => '{"name": "${value.name}", "id": ${value.id}}';

  static Category? fromJson(Map<String, dynamic> json) {
    if (json case {'name': String categoryName, 'id': int id}) {
      return Category((name: categoryName, id: id));
    }
    return null;
  }
}

// Library 'main.dart'.
// import 'user.dart';
// import 'category.dart';

void main () {
  final joseph = User((id: 1, name: "Joseph"));
  final news = Category((id: 1, name: "News"));
  final danny = User.fromJson({"name": "Danny", "id": 2 });
  final nature = Category.fromJson({"name": "Nature", "id": 2 });

  print(joseph);
  print(news);
  print(danny);
  print(nature);

  print(joseph?.toJson());
  print(news?.toJson());
  print(danny?.toJson());
  print(nature?.toJson());  

  // If we change `final` to `var`, these are still compile-time errors:
  // joseph = news;
  // news = joseph;
  // danny = nature;
  // nature = danny;
}

This runs and yields the following output:

(id: 1, name: Joseph)
(id: 1, name: News)
(id: 2, name: Danny)
(id: 2, name: Nature)
{"name": "Joseph", "id": 1}
{"name": "News", "id": 1}
{"name": "Danny", "id": 2}
{"name": "Nature", "id": 2}
rrousselGit commented 1 year ago

@rubenferreira97 That is correct, yes. But I'd blame the example. I don't think the original example of a "User" vs "Category" is good.

As a general rule, I'd say that if two typedefs define the same record, then their utilities should be interchangeable. Otherwise these probably shouldn't be records but classes instead.

AlexVegner commented 1 year ago

Dart 3 is great. Huge thanks for Dart team! Records brings us close to minimalistic data class. But we still not there. It will be nice to have ability to create final class on top of record exampe:

data class User extends ({String? firstName, String? lastName}) {
   static User? fromJson(Map<String, dynamic> json) {
    return switch (json) {
      {
        'firstName': String? firstName,
        'lastName': String? lastName,
      } =>
        (firstName: firstName, lastName: lastName),
      _ => null,
    };
  }

  Map<String, dynamic> toJson() {
    final (:firstName, :lastName) = this;
    return {'firstName': firstName, 'lastName': lastName};
  }
}

where record provide default constructor, and all records abilities, and devs can add more functionality

final u1 = User(firstName: 'A', lastName: 'V'); 
final (:firstName, :lastName) = u1;
final u2 = User.fromJson({'firstName': 'A', 'lastName': 'V'});
final u3 = (firstName: 'A', lastName: 'V') as User;
assert(u1 == u2);
assert(u1 == u3);
assert(u1 is User);
assert(u1 is ({String? firstName, String? lastName}));

extension CategoryExt on Category { static Category? fromJson(Map<String, dynamic> json) { return switch (json) { { 'name': String name, 'id': int id, } => (name: name, id: id), _ => null, }; }

Map<String, dynamic> toJson() { final (:name, :id) = this; return {'name': name, 'id': id}; } }

final c1 = Category(name: "C", id: 1); final c2 = Category.fromJson({'name': 'A', 'id': 1}); assert(c1 == c2); assert(c1 is Category); assert(c1 is ({String name, int id}));



+ Also **spread operator support for records** (also nice to have) can replace copyWith. 
https://github.com/dart-lang/language/issues/1292#issuecomment-1545980228
lrhn commented 1 year ago

If we get primary constructors (#2364) and inline classes (#2727), and the ability for those to expose their representation type members, and maybe allow treating record types as if they have an unnamed constructor taking their fields as arguments, that could perhaps (very speculatively, using syntax and functionality that I just invented) look like:

final inline class User({super.firstName, super.lastName}) extends ({String? firstName, String? lastName}) {
  static User? fromJson(Map<String, dynamic> json) => switch (json) {
      {
        'firstName': String? firstName,
        'lastName': String? lastName,
      } => User(firstName: firstName, lastName: lastName),
      _ => null,
    };

  Map<String, dynamic> toJson() => {'firstName': firstName, 'lastName': lastName};
}
AlexVegner commented 1 year ago

@lrhn It will be nice to keep parent record signature for primary constructor. Record already provide signature for primary constructor, any primary constructor modification will broke record construction interface. And it can prevent ability User class behave like the paent record

final inline class User extends ({String? firstName, String? lastName}); 
// aslo nice to have ; instead of {} for empty body it will be also usefull for sealed class
final u1 = (firstName: 'A', lastName: 'V') as User;
final (:firstName, :lastName) = u1;
assert(u1 is ({String? firstName, String? lastName}));

or similar to kotlin data class, we can have statically typed records.

record class User({String? firstName, String? lastName}) {}

record class Employee({String? firstName, String? lastName}) {}

record class Point({int x, int y}) {}

final record = (firstName: 'A', lastName: 'V');
final user = record as User;
final e1 = record as Employee;
final e2 = user as Employee;
// make a copy and mutate with spread operator support 
final e3 = (...e1, firstName: 'D'); // Auto cast to Employee
final e4 = (...record, firstName: 'D') as Employee;
final e5 = (...user, firstName: 'D') as Employee;

assert(user != record); // different type
assert(e1 == e2); // same type ans data after cast
assert(e2 != user); // different type
assert(e3 == e4);
AlexVegner commented 1 year ago

I have created separate ferature proposal for it (https://github.com/dart-lang/language/issues/3075)

zhxst commented 12 months ago

It would be great to allow extension static field on a class. Currently extension A on B with static field is kind of useless. If anybody want to call A.something, a class A will do the thing.

cedvdb commented 10 months ago

Agreed. What is the use case, if any, in having a static method on the extension namespace instead of the on-class namespace ?

rrousselGit commented 10 months ago

You mean static methods on plain extension, not static extension?
They can be used inside methods:

extension on Foo {
  static void fn() {}

  void someExtensionMethod() {
    fn();
  }
}

Although IMO the value is very low, and it is an unfortunate usage of the keyword.

rrousselGit commented 10 months ago

Maybe it'd be useful to have a script checking all extensions in the wild and see if they have static members?

cc @munificent – sounds like the sort of things you like doing :D