dart-lang / language

Design of the Dart language
Other
2.65k stars 202 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) {}
}
rrousselGit commented 4 years ago

If you have ParseDuration.parse as a "constructor", it would be kind of misleading if that was available as Duration.parse.

That wouldn't be the case. We currently can use the static keyword, but not factory.

MyExtension.someFactory shouldn't be a thing

rrousselGit commented 4 years ago

There's still a difference between static methods and factories.

Factories can do:

abstract class Foo {
  const factory Foo.myFactory(int a) = _FooImpl;
}

class _FooImpl implements Foo {
  const _FooImpl(int a);
}

Which is the only way to have const constructors on factory.

That's a bit off-topic though.

lrhn commented 4 years ago

As has been pointed out, static is already allowed in extensions, and it creates static methods in the extension namespace, not on the on-type.

Class declarations introduce a namespace too, and for a generic class, the distinction between a type and the class is fairly big. The class declaration List is not represented by the type List<int> or even List<dynamic> (or in the future: List<dynamic>?). Adding static methods or constructors to a class namespace would need to be declared on a class rather than a type, which is what the on type of an extension declaration is.

That is one reason for not allowing static methods of extension declarations to be accessible on the on type: The on type of an extension declaration can be any kind of type, not just class types. There is not necessarily a namespace that the methods can go into. Putting a static method on int Function(int) or List<int> (but not List<String>) would leave you with no way to call that static method.

We could consider new syntax to actually inject static members into other classes, say static extension on List { int get foo => 42; }, which would only allow a class name as on target.

[!NOTE] If we have extension statics, then it's possible to declare a static member with the same name as an instance member on the same type, as long as at least one is an extension member. We should probably allow declaring that inside the same class declaration, rather than pushing people to use extensions to achieve the same thing indirectly. (Maybe augmentations can allow the same thing, which is another argument for just allowing it directly.) #1711

rrousselGit commented 4 years ago

That request doesn't ask for allowing static methods on List<int> though, but factory.

I do agree that it doesn't make sense to use the static keyword on a generic on. But I think it does make sense for factory.

Does your comment apply to factory too?

donny-dont commented 4 years ago

Adding factory to a class would be a really nice thing to have. In terms of real world examples it would be nice for browser apps when doing CustomEvents.

extension MyEvent on CustomEvent {
  factory CustomEvent.myEvent(String message) {
    return CustomEvent('my-event', message);
  }

  String get message {
    assert(type == 'my-event', 'CustomEvent is not 'my-event');
    return detail as String;
  }
}

I was also thinking of using it for a http like library.

extension JsonRequest on Request {
  factory Request.json(Map<String, dynamic> body) {
    return Request(jsonEncode(body), headers: { 'Content-Type': 'application/json' });
  }
}
lrhn commented 4 years ago

A factory declaration is not as bad as a pure static method because it does provide a way to give type arguments to the class. It's still vastly different from an extension method.

If you define:

extension X on Iterable<int> {
  factory X.fromList(List<int> integers) => ... something ...;
}

then what would it apply to? Most likely it would only apply to Iterable<int>.fromList(...). That means no subtyping, because List<int>.fromList(...) would probably not create a list, only an iterable. But perhaps super-types, because Iterable<num>.fromList([1, 2, 3]) would create an iterable of numbers. That's completely different from how extension methods are otherwise applied, where the method applies to any subtype of the on type, so again I think it deserves its own feature. It's not just an extension of extension.

The on type of that feature must be a class because you can only write new ClassName<typeArgs>(args) to invoke it, and there is no syntax which allows this on int Function(int).

All in all, I'd prefer something like:

static extension Something<T> on ClassType<T> {
  static int method() => 42;
  factory Something.someName() => new ClassType<T>.actualConstructor();
}

which effectively adds static members and constructors to a class declaration, and can be hidden and imported just like extensions.

It can probably do one thing normal constructors cannot: Declare a constructor only on a subtype, so:

static extension X on List<int> {
  factory X.n(int count) => List<int>.filled(count, 0);
}
... List<int>.n(5) ...

(I would support adding extra restrictions to type arguments in constructors in general too.)

rrousselGit commented 4 years ago

Nice catch on the subclass thing.

For me, factory/static extensions should be applied only on the extact match of the on clause.

Such that with:

static extension on A {
  factory something() = Something;

  static void someMethod() {}
}

Would be applied only on A but not on a subclass of A. This would otherwise introduce a mecanism of static method inheritance, which gets kinda confusing and likely unexpected.

The on type of that feature must be a class because you can only write new ClassName(args) to invoke it, and there is no syntax which allows this on int Function(int).

What about requiring a typedef, and applying the extension only typedefs then?

Because typedefs makes it definitely useful. I would expect being able to write:

typedef VoidCallback = void Function();

static extension on VoidCallback {
  static empty() {}

  const factory delayed(Duration duration) = CallableClass;
}

VoidCallback foo = VoidCallback.empty;
const example = VoidCallback.delayed(Duration(seconds: 2));

Currently, Dart lacks a way to namespace functions utils

rrousselGit commented 4 years ago

@tatumizer With such syntax, how would you represent generic extensions?

static extension<T extends num> on List<T> {
  factory example(T first) => <T>[first];
}
lrhn commented 4 years ago

If the static extension itself does not have a name, then it's not possible to hide it if it causes conflicts. That's why extension declarations have names. I'd want that for static extensions too.

ds84182 commented 4 years ago

I think static extension methods would somewhat help with #746. However, there may be a couple of things that might be confusing, like how type inference affects constructor availability.

e.g.

static extension IntList on List<int> {
  factory List<int>([int length]) => List.filled(length ?? 0, 0);
}

static extension NullableList<T> on List<T?> {
  factory List<T?>([int length]) => List.filled(length ?? 0, null);
}

etc.

For constructor availability, it feels weird to have to specify the type to get the correct factory constructors. For example:

List<int> foo;

foo = List(123);

Also, if you have to disambiguate between multiple extensions, how would the syntax look? IntList(123)? This expands to new IntList(123), but the return type isn't IntList (because it isn't a type, it's a member).

leonsenft commented 4 years ago

I just wanted to voice support for adding static members to an existing class. For what it's worth I don't care about factory constructors since static methods are virtually indistinguishable at call sites anyways.

For context, I think this would help improve certain code generators that want to generate top-level members based on an existing types. Rather than requiring users to remember a particular naming convention to find these members, they could instead be added as extensions to their associated class.

For example, AngularDart compiles developer-authored components into a number of view classes. We expose a ComponentFactory instance from the generated code that is used to instantiate a component and its corresponding view dynamically:

// Developer-authored
// example_component.dart

@Component(...)
class ExampleComponent { ... }
// Generated
// example_component.template.dart

ComponentFactory<ExampleComponent> createExampleComponentFactory() => ...;
// main.dart

import 'example_component.template.dart';

void main() {
  // User has to remember this naming convention.
  runApp(createExampleComponentFactory());
}

Ideally we could declare these ComponentFactory functions as static extension methods on their corresponding component class instead of putting them in the top-level namespace. I think this offers better ergonomics and readability:

// Generated
// example_component.template.dart

static extension ExampleComponentFactory on ExampleComponent {
  ComponentFactory<ExampleComponent> createFactory() => ...;
}
// main.dart

import 'example_component.template.dart';

void main() {
  runApp(ExampleComponent.createFactory());
}
bitsydarel commented 4 years ago

I was just adding extensions to one of my library.

first i tried to move factory methods to an extension but got error that factory are not supported and i was like that's fine.

Then tried to convert factory constructor to static functions with factory annotations, the IDE did not complain but the client code using the extension did complain.

I was expecting static methods to be supported because most of the language i have used support it...

bitsydarel commented 4 years ago

Also the current behavior of defining static extension methods is not clear.

example:

Previous API definition.

class ExecutorService {
  factory ExecutorService.newUnboundExecutor([
    final String identifier = "io_isolate_service",
  ]) => IsolateExecutorService(identifier, 2 ^ 63, allowCleanup: true);
}

With the new extension feature.

extension ExecutorServiceFactories on ExecutorService {
  @factory
  static ExecutorService newUnboundExecutor([
    final String identifier = "io_isolate_service",
  ]) => IsolateExecutorService(identifier, 2 ^ 63, allowCleanup: true);
}

Currently the client code have to call it this way:

ExecutorServiceFactories.newUnboundExecutor();

But i think the syntax should be:

ExecutorService.newUnboundExecutor();

It's more common for people that are use to extensions in other languages, it's more clear on which type its applied, its does not create confusion in client code, it's does not the break the API of library and does not require code change on client side.

natebosch commented 4 years ago

I'm not sure if it has been mentioned yet. Adding this support would increase the set of changes which are breaking.

As of today it is not a breaking change to add a static member on a class. If we give the ability to add members to the static interface of a class we need to come up with how to disambiguate conflicts. If we disambiguate in favor of the class it's breaking to add a static member, because it would mask a static extension. I'm not sure all the implications of disambiguating in favor of the extension.

ThinkDigitalSoftware commented 4 years ago

As has been pointed out, static is already allowed in extensions, and it creates static methods in the extension namespace, not on the on-type.

At this point, the extension is useless, since it's just a class with static members. for calling PlatformX.isDesktop, the following 2 snippets produce the same results.

extension PlatformX on Platform {
  static bool get isDesktop =>
      Platform.isMacOS || Platform.isWindows || Platform.isLinux;
  static bool get isMobile => Platform.isAndroid || Platform.isIOS;
}
class PlatformX {
  static bool get isDesktop =>
      Platform.isMacOS || Platform.isWindows || Platform.isLinux;
  static bool get isMobile => Platform.isAndroid || Platform.isIOS;
}
rrousselGit commented 4 years ago

The purpose of using extensions for static methods and factories is to regroup everything in a natural way.

Sure, we could make a new placeholder class. But then you have to remember all the possible classes that you can use.

patrick-fu commented 4 years ago

The purpose of using extensions for static methods and factories is to regroup everything in a natural way.

Sure, we could make a new placeholder class. But then you have to remember all the possible classes that you can use.

regroup everything in a natural way Yes, that's the point!👍


I found this problem when making a flutter plugin. I plan to put static methods and static callback Function members in the same class for the convenience of users, but on the other hand, I want to move the callback to another file to Improve readability.

I found that dart 2.6 supports extensions. I thought it was similar to swift, but when I started to do it, I found various errors. After searching, I regret to find that static method extensions are not supported.🥺

extension ZegoEventHandler on ZegoExpressEngine {
    static void Function(String roomID, ZegoRoomState state, int errorCode) onRoomStateUpdate;
}
  void startLive() {
    // Use class name to call function
    ZegoExpressEngine.instance.loginRoom("roomID-1");
  }

  void addEventHandlers() {
    // I want to use the same class to set the callback function, but it doesn't work
    // ERROR: The setter 'onRoomStateUpdate' isn't defined for the class 'ZegoExpressEngine'
    ZegoExpressEngine.onRoomStateUpdate =
        (String roomID, ZegoRoomState state, int errorCode) {
      // handle callback
    };

    // This works, but requires the use of extended aliases, which is not elegant
    ZegoEventHandler.onRoomStateUpdate =
        (String roomID, ZegoRoomState state, int errorCode) {
      // handle callback
    };
  }

At present, it seems that I can only use the extension name to set the callback function, which can not achieve the purpose of letting the user only pay attention to one class.🧐

jlubeck commented 4 years ago

Definitely agree that the static method should be called from the original class and not the extension class.

faustobdls commented 4 years ago

@rrousselGit I think the same, in my case the intention is to use to apply Design System without context doing this way:


extension ColorsExtension on Colors {
  static const Color primary = const Color(0xFFED3456);
  static const Color secondary = const Color(0xFF202426);

  static const Color backgroundLight = const Color(0xFFE5E5E5);
  static const Color backgroundDark = const Color(0xFF212529);

  static const Color warning = const Color(0xFFFFBB02);
  static const Color warningBG = const Color(0xFFFFFCF5);
  static const Color confirm = const Color(0xFF00CB77);
  static const Color confirmBG = const Color(0xFFEBFFF7);
  static const Color danger = const Color(0xFFF91C16);
  static const Color dangerBG = const Color(0xFFFEECEB);

  static const MaterialColor shadesOfGray = const MaterialColor(
    0xFFF8F9FA,
    <int, Color>{
      50: Color(0xFFF8F9FA),
      100: Color(0xFFE9ECEF),
      200: Color(0xFFDEE2E6),
      300: Color(0xFFCED4DA),
      400: Color(0xFFADB5BD),
      500: Color(0xFF6C757C),
      600: Color(0xFF495057),
      700: Color(0xFF495057),
      800: Color(0xFF212529),
      900: Color(0xFF162024)
    },
  );
}
creativecreatorormaybenot commented 4 years ago

@faustobdls In that case, it seems pretty counterintuitive to me to do what you are proposing for these two reasons:

  1. If Colors.primary is used in your code, it might appear as though the material Colors class declares this primary color. However, this is not the case! You declare it for you own design yourself. Why do you not give your class a more telling name instead of wanting to add to Colors, like CustomDesignColors. You could even make that a mixin or extension with the current implementation.

  2. What should happen when the material Colors class is updated and now declares members with the same names?

faustobdls commented 4 years ago

@creativecreatorormaybenot my intention is the use of this for flavors within the company, definition of design system and etc, besides that this is just a case, we can mention others that depend on the fact that the static andfactory methods can be used by others , recent we wanted to make a class Marker a toJson () and a fromJson () and fromJson () is static we had to create another class for this, but with extension it would be much better and more readable, at least in my opinion

listepo commented 4 years ago

Any news? Very necessary feature

marcglasberg commented 4 years ago

@creativecreatorormaybenot Extension methods as a whole are convenience/style feature. Since they were implemented they should be implemented right, and this is missing. Also nobody said it would be prioritized over NNBD. Don't worry, NNBD will not be delayed because of this issue, if that's what you fear, somehow.

lrhn commented 4 years ago

Another idea.

How about allowing extensions on multiple types, and on static declaration names, in the same declaration:

extension Name 
   <T> on List<T> {
      .... normal extension method ...
   } <T> on Iterable<T> { 
      ...
   } <K, V> on Map<K, V> {
     ...
  } on static Iterable { // static methods.
    Iterable<T> create<T>(...) => 
  } 

I'd still drop constructors. It's to complicated to make the distinction between a constructor on a generic class and a generic static method. Allowing that does not carry its own weight.

By combining the declarations into the same name, we avoid the issue of having to import/export/show/hide two or more names for something that's all working on the same types anyway.

Having the same name denote different extensions does make it harder to explicitly choose one. If I do Name(something).method(), it will still have to do resolution against the possible matches. Is it a list or iterable? What id it's both a map and a list?

MarvinHannott commented 4 years ago

There is another unexpected problem extension methods cause, but this is also one they could, with @rrousselGit proposal, fix. The problem is that extension methods only exist when the generic type argument is known. But in a generic class' constructor we don't know the concrete type. I try to explain: I was trying to write a thin wrapper type for Dart's FFI:

class Array<T extends NativeType> {
  Pointer<T> ptr;
  List _view;
  Array(int length) // Problem: ptr.asTypedList() only exists with known type T
}

Pointer<T> has various extensions like Uint8Pointer on Pointer<Uint8> which defines asTypedList() -> Uint8List. But in the constructor of Generic<T> I don't know the concrete type, so I can't call asTypedList(). This would be trivial to solve with C++ templates, but Dart makes this trivial seeming problem very difficult to solve.

With @rrousselGit proposal this problem could be easily solved:

class Array<T extends NativeType> {
  Pointer<T> ptr;
  List _view;
  Array._(this.ptr, this._view);
}
extension Uint8Array on Array<Uint8> {
  Uint8List get view => _view;
  factory Array(int length) {
    final ptr = allocate<Uint8>(count: length);
    return Array._(ptr, ptr.asTypedList(length));
  }
}
extension Int16Array on Array<Int16> {
  Int16List get view => _view;
  factory Array(int length) {
    final ptr = allocate<Int16>(count: length);
    return Array._(ptr, ptr.asTypedList(length));
  }
}

Array<Uint8>(10); // works
Array<Int16>(10); // works
Array<Uint32>(10); // doesn't work
Array(10); // doesn't work

But for now I have to use static extensions which are a heavy burden on users:

extension Uint8Array on Array<Uint8> {
  Uint8List get view => _view;
  static Array<Uint8> allocate(int length) {
    final ptr = allocate<Uint8>(count: length);
    return Array._(ptr, ptr.asTypedList(length));
  }
}
extension Int16Array on Array<Int16> {
  Int16List get view => _view;
  static Array<Int16> Array(int length) {
    final ptr = allocate<Int16>(count: length);
    return Array._(ptr, ptr.asTypedList(length));
  }
}

Uint8Array.allocate(10);
Int16Array.allocate(10);

This is just confusing because a user would think Uint8Array would be another type. And now he has to remember all these extensions to create an Array.

Subclassing would not be a good solution, because then Array<Uint8> couldn't be passed where Uint8Array is expected. They would be incompatible. Java programmers make this mistake all the time because they lack type aliases. In fact, it would be really cool if we could use extensions as type aliases:


Uint8Array arr = Uint8array.allocate(10); // synonymous to Array<Uint8> arr;
omidraha commented 4 years ago

I have the same issue while wanting to add static method as extension to the User model of sqflite.

I would like to have something like this:

User user = await User.get(db);

But currently I have to:

User user = await User().get(db);
jpgpuyo commented 3 years ago

I have the same issue when trying to create a extension function from network images. If we consider dart as a serious language, this feature should be implemented.

AlexVegner commented 3 years ago

It will also help with cleaner code generation I like to use extension for code generation (for example mutable_copy, copy_with) Support of static, factory (and even override) for extension will add more possibilities for code generation libraries.

Just a sample:

@JsonSerializableExtension()
@EquatableExtension()
@imutable
@MutableCopy()
@CopyWith()
// And many more possible extension
class User {
  final String name;
  final String email;

  User({this.name, this.email});
}

generate code:

extension $UserJsonSerializableExtension on User {
  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

extension $UserEauatableExtension on User {
  @override
  bool operator ==(Object other) => _$UserEqual(this, other);

  @override
  int get hashCode => _$UserHashCode(this);
}

...

Model will looks cleaner.

We even will have ability to implement equivalent of kotlin data class with one annotation.

kotlin

data class User(val name: String, val email: String)

dart

@DataModel()
class User {
  final String name;
  final String email;

  User({this.name, this.email});
}
lukepighetti commented 3 years ago

Just wanted to provide a concrete example of where I would use this.

extension SizedBoxX on SizedBox {
  static SizedBox square(double dimension, {Widget child}) => SizedBox.fromSize(
    size: Size.square(dimension),
    child: child,
  );
}
return SizedBox.square(16);
lrhn commented 3 years ago

I did a quick write-up of a proposal strawman, which tries to address at least one of the pain points of having to declare extension static members separately from extension instance members (by allowing the extensions to share the same name).


Dart Extension Static Methods

Dart has static extension methods which allows declaring static functions which can be invoked as if they were non-virtual instance methods on specific types. Users have requested a way to also add extension static methods to specific classes, which the current feature does not provide.

This is a proposal for a generalized syntax for extensions which include a way to add static functions which can be invoked as if they were static members of a specific namespace declaration.

Extension Static Methods

The syntax for extension (instance) methods is:

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

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;
}

The name after static must be the name of a class or mixin declaration. (We do not allow extension statics to be declared on another extension declaration, which is the third place where you can currently declare static members.) The static declarations must be static method, getter or setter declarations, static constant declarations, or factory constructor declarations, are then made available as DeclName.looksLikeStaticMethod(), DeclName.myConst or new DeclName.name().

For factory constructors, both DeclName and Name are allowed as the name before the .. (I find Name more correct, but really, really want to write DeclName when I write the code. If we changed the language syntax to allow new, instead of repeating the class name, for all constructors, then that headache would go away!)

The type parameter on the class is only necessary when using a constructor, it gives a name to the type provided in new DeclName<int>.name(), and is available inside the constructor. It’s not available in static methods. Another alternative would be writing it as factory DeclName<T>.name() …. In that case, it makes little sense to use Name instead of DeclName since it’s DeclName‘s type parameters we refer to.

If DeclName already has a static member with the same base name, then it takes precedence over the extension. NOTICE: An instance member does not shadow the extension. We will get back to this.

If two extension statics both add static members with the same name on the same declaration, accessing that name is a compile-time error unless all-but-one of them are declared in platform libraries.

The static members are also available as static members of Name, so you can invoke Name.looksLikeStaticMethod() and new Name.name() directly to circumvent naming conflicts (or just not import one of the extensions as usual).

Combined Extensions

It will likely be common to declare both static and instance extensions on (roughly) the same type, and it’s already common to declare multiple related extensions on different types. It’s annoying to have to hide or show more than one name, so we allow multiple extensions to share the same name.

extension Name on static DeclName {
  static ...
} <T> on DeclName<T> {
  ...
} <T> on SubOfDeclName<T> {
  ...
} on static SubOfDeclName {
  static ... 
}

These are treated as separate extensions for resolution, but are all hidden/shown as one.

Since the extensions have the same name, we now need conflict resolution for the explicit extension application syntax: Name(e).foo. There may now be more than one extension named Name which applies, and we’ll use the normal extension resolution to choose between those (and potentially fail). It’s up to the individual combined extension author to not create situations where conflict resolution will fail.

Another option is to only allow at most one one non-static section in each declaration. Or even only allow at most one non-static and at most one static section, with the intent that the two should be on the “same” type. That reduces or removes the risk of name conflicts.

Same-named static and instance members

Currently Dart does not allow a class to have a static member and an instance member with the same base name. If a class’s interface declares or inherits an instance member, it cannot declare a static member with the same base name. Since static members are not inherited, a subclass can declare a new member with the same name as a superclass static member.

With extension static members, you can do something which resembles that:

class C {
  int get foo => 42;
}
extension CFoo on static C {
  int get foo => 37;
}
void main() {
  print(C().foo); // 42
  print(c.foo); // 37
}

Giving authors this power indirectly risks them using it, declaring a class and a static extension on the class right next to each other. The only real difference between this and allowing the class to declare the static directly is the added risk of another extension conflicting with the name, which such authors will likely ignore.

That suggests that we should allow a class to declare a static member and an instance member with the same base name, rather than making that remain a conflict. That is:

If there is only one declaration, the name always refers to that, so it’s not exactly like having two nested scopes. It’s more like a scope with two possible values for each name, and a lookup which prefers one over the other.

Examples

extension MyEnumX on static MyEnum {
  factory MyEnumX.fromString(String name) => 
      MyEnum.values.firstWhere((s) => s.endsWith(".$name"));
} on MyEnum {
  MyEnum get next => MyEnum.values[(this.index + 1) % MyEnum.values.length];
}
creativecreatorormaybenot commented 3 years ago

@tatumizer You can already declare static methods on extensions, which will allow FooExtension.barMethod.

Example:

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

void main() {
  Foo.bar();
}

This is why there is a need for on static.

creativecreatorormaybenot commented 3 years ago

@tatumizer That would be a breaking change, however. It is not possible to do this now without potentially breaking existing code as in existing code the static method might have the name of a static method of the original class.

lukepighetti commented 3 years ago

is there any reason why we couldn't infer this

class C {
  int get foo => 42;
}
extension CX on C {
  int get bar => 43;
}
extension StaticCX on static C {
  int get foo => 37;
}

from this?

class C {
  int get foo => 42;
}
extension CX on C {
  int get bar => 43;
  static int get foo => 37;
}
AlexVegner commented 3 years ago

@creativecreatorormaybenot I don't see any breaking changes in described sample. All static colisions can be resolved with error like for extension method.

extension Foo on Object {
  String toString() => 'string';
}

Error: This extension member conflicts with Object member 'toString'

AlexVegner commented 3 years ago

@lukepighetti

class A {
  static String toString() {
    return 'string';
  }
}

Shows an error: Error: Can't declare a member that conflicts with an inherited one. static String toString()

So for the sample:

class C {
  int get foo => 42;
}
extension CX on C {
  int get bar => 43;
  static int get foo => 37;
}

We should see same conflicts error for foo Now dart doesn't show such error, so it will be breaking changes.

creativecreatorormaybenot commented 3 years ago

@AlexVegner The problem is that this was allowed before. This means that you would break existing code.

The following is currently allowed:

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

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

void main() {
  Foo.bar(); // 'baz' with existing functionality
}

With what @tatumizer proposed, the existing code breaks. It either breaks because it overrides the Foo static bar, which it did not before or it breaks because there is an error now.

creativecreatorormaybenot commented 3 years ago

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

AlexVegner 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) 
lrhn commented 3 years ago

The way I wrote it, members declared on the real class takes precedence, so the above example will still print baz. Extension members only apply if the expression would otherwise be an error because there is no member with the given name.

It's possible to allow extension static members to be accessible on both the type and the extension. That's not why I am separating the extension statics from the extension instance members. It's because it doesn't make sense to put static members on all types. Examples:

extension E1<T> on T { static int get foo => 42; }
extension E2 on List<int> { static int get bar => 42; }
extension E3 on int Function(int) { static int get baz => 42; }

In all three examples, the type is not (necessarily) a class or mixin declaration. There is no way to call bar on List<int> because List<int> does not denote a declaration. Neither does T or a function type. Even if you did:

typedef F = int Function(int);
extension E4 on F { static int get qux => 42; }
enum Enum {bif, baf, buff}
extension E5 on Enum { static int get floo => 42; }

I would not allow you to write F.qux or Enum.floo. Typedefs and enums cannot hold statics to begin with, so extending them with statics is a completely new thing. (We should consider allowing static declarations inside enums, but we should consider a lot of things about enums).

So, the on static is there because the target of a static extension is a (class or mixin) declaration, the target of an instance extension is a type. There are lot of types which do not denote a class. Also, statics are not inherited, so putting a static extension on Iterable does not mean it is also on List.

If we just said that extension statics only worked when the target was a class or mixin type, then we still had to figure out when that is. Is it only when the on type is literally ClassName<optionally type args>? That could work, and it's probably the only thing which can. If the extension is <T extends ClassName<int>> on T then that wouldn't trigger statics on ClassName.

So, if we want that, then the change relative to today would be:

If the on type of an extension e with name E is ClassName or ClassName<type args>, then static declarations of the extension can also be accessed as ClassName.staticName iff the class does not already declare a static member with the same base name as staticName, and the extension may also declare factory constructors (the class name used in the factory constructor declaration may be either ClassName or E). If multiple accessible extensions declare static members or constructors with the same name which apply to the same class, it's a compile-time error to access the members through the class. They can still be accessed directly on the extension.

My fear about this, and the reason I didn't do it directly, is that it's going to be confusing to users that some extensions work that way, and others do not. They'll start expecting it to work on enums (OK, it probably should work on enums, even if we haven't allowed normal enum statics yet, it's too silly that it doesn't) or on typedefs (not going to happen), or on things that resolve to a class type, but isn't written as one. Maybe that's just an educational issue.

eernstg commented 3 years ago

@lrhn wrote:

then the change relative to today would be: [basically, allow static methods declared by the extension to be accessed using the class/mixin of the on type, and allow factories]

That's very nice! I think it would be much more manageable than having scopes with multiple lookup values per name.

lukepighetti commented 3 years ago

There's List<int>.from(...). Why can't we have a static extension method on List<int> like List<int>.numbers(from: 1, to: 8)

lrhn commented 3 years ago

I've considered the "use statics of extension as static on extended interface" approach some more. One issue is that we need type arguments for the factory constructors. Say:

extension NumList<T extends num> on List<T> {
   factory List.zero(int count) {
     T zero = 0 is T ? 0 as T : 0.0 as T;
     return [for (var i = 0; i < rows; i++) zero];
  }
}

If we write List.zero(5), how does it work? We need to bind T somehow, so let's do inference on the constructor invocation as if it was a normal one. If there is a hint, we use that, if there isn't we intantiate-to-bounds, but we do it on the extension instead of the class.

var l1 = List<int>.zero(5);  // Infer T as we would for `NumList(_ as List<int>)`
List<int> l2 = List.zero(5);  // Infer `List<int>.zero(5)` as we would for other constructors, then see l1.
var l3 = List.zero(5);  // No hints, do I2B on the extension and infer `List<num>.zero(5)`.
Object l4 = List.zero(5);  // Same, context type, but no hint to type argument.
var l5 = List<String>.zero(5); // Compile-time error, extension does not apply to `List<String>`

I think this can work.

This example is easy because the type argument is just passed right through. It could also be:

extension Matrix<T extends num> on List<List<T>> {
  List.zero(int cols, int rows) {
     T zero = 0 is T ? 0 as T : 0.0 as T;
     return [for (var i = 0; i < rows; i++) [for (var i = 0; i < cols; i++) zero]];
  }
  List<List<T>> vectorMultiply(List<T> vector) => ...;
}

Then you can write:

  var m = Matrix<int>.zero(2, 5);  // You can use the extension name for constructors.
  var m2 = m.vectorMultiply([1, 2 ,3, 4, 5]);

but if you write:

  List<List<int>> m = List.zero(2, 5);

we still need to infer that it is List<List<int>>.zero(2, 5) and that T is int when we execute the factory.

rrousselGit commented 3 years ago
var l3 = List.zero(5);  // No hints, do I2B on the extension and infer `List<num>.zero(5)`.

I don't think that should be inferred as num here, but int instead.

Otherwise that would behave differently from normal constructors:

class Example<T extends num> {
  Example(T value);
}

extension NumList<T extends num> on Example<T> {
  factory Example.myFactory(T value);
}

...

var a = Example(5); // Example<int>
var b = Example.myFactory(5); // Example<num>
lrhn commented 3 years ago

The argument of NumList.zero is a count, not an element, so List.zero(5) has no hint to the element type. That's why I'd let it instantiate-to-bounds to num.

It's true, though, that in normal constructors we'd let the argument type be used for inference, but we generally don't look at the argument types for instance extension methods.

If we had:

extension Foo<T extends num> on List<T> {
  factory List.single(T value) => <T>[value];
}

then List.single("a") would be an issue. We don't want to infer the type o the argument more than once (it could be a large expression instead of a single literal), but if the type of the argument decides which constructor is invoked, then we have a conundrum.

So, it's probably safer and more performant (even if less useful) to decide that a constructor applies simply from its name, not the type it accepts, and then afterwards try to infer the type for the one constructor we end up using. That can then turn out to be an invalid type, in which case it's a compile-time error. So, no having List.zero on both List<int> and List<double> - the two will conflict and you can't use the type to choose (no List<int>.zero(5) and List<double>.zero(5) choosing different constructors, both will just be name conflicts. Too bad, I liked the idea :)

listepo commented 3 years ago

Any news?

lslv1243 commented 3 years ago

Dart would be more intuitive if types behaved like an instance of its own type, and anything that can be done to an instance could be done to a type. for example, this:

class Test {
  int a = 0;
  static var a = 1;
}

extension Test_ on Test {
  static int get b => 2;
}

void main() {
  final test = Test();
  print(test.a);
  print(Test.a);
  print(Test.b);
  // today it does not work
}

could desugar to behave like this:

class TestThis {
  var a = 1;
}

class Test {
  int a = 0;
  static final This = TestThis();
}

extension TestThis_ on TestThis {
  int get b => 2;
}

void main() {
  final test = Test();
  print(test.a);
  print(Test.This.a);
  print(Test.This.b);
  // prints 012
}

I just don't know how static const would work 😅

eernstg commented 3 years ago

@lslv1243 wrote:

anything that can be done to an instance could be done to a type

For this, I think it's relevant to revisit an old idea.

You could say that we have this property already: If T denotes a type and is also an expression then you can evaluate it and get hold of an instance of Type that reifies the type T. int is an example of this, and so is myImportPrefix.MyClass. List<int> is not (considered as an expression, that's a syntax error), but a type variable will do, and the value of a type variable can be any type.

So let's say that t is an instance of Type that reifies a class C (or C<T1..Tk> if C is generic—when we discuss static members we don't care about the type arguments).

t is an object, so it has all the affordances that objects have; it's of type Type, though, which means that there aren't many members in its interface, and hence you can only do a few things, e.g., t == something and t.toString().

It has been proposed that t should have an instance member corresponding to each static member of C, which would allow us to invoke static members of C as instance members of t, which could very well be the main point that you wish to make by using the phrase 'anything that can be done to an instance could be done to a type'.

The main reason why that proposal was never made part of the language is that it fits really badly with static typing: You'd need to use (t as dynamic).m() in order to call the static method m, because t has type Type, and Type doesn't have an instance member named m.

So you could say that Type has a type argument, and Type<C> (resp. Type<C<T1..Tk>>) is the type of T. But then you'd need completely new type rules in order to be able to recognize that Type<C> has a lot of members (corresponding to the static members in C), and Type<D> has a completely separate set of members (corresponding to the static members in D), and, even worse, Type<X> where X is a type variable would not make sense at all, and we couldn't allow Type<D> to be a subtype of Type<C> even in the case where D <: C. The properties of static members and instance members are too different to fit into a setup like this.

In summary: There is an easy way to achieve something which seems to be quite similar to what you're requesting (let the reified class have instance methods corresponding to the static methods of the class). But it is incompatible with static typing, so that proposal didn't make it into the language.

lslv1243 commented 3 years ago

@eernstg I believe there is no need for Type to have a type argument. You could make the type class, C in your example, a subclass of Type, thus allowing the cast. I believe Swift handles it the way I described:

class A {
    let a = "instance member"
    static let a = "static member"
}

class B<T> {
    // created this scope to have some type that is unknown to imitate the Dart `Type`
    func unknownTypeScope() -> String {
        // we check if the type is the one we were expecting
        if (T.self is A.Type) {
            // we cast the unknown type and have access to the static member as an instance member
            let type = T.self as! A.Type
            return type.a
        } else {
            return "other type"
        }
    }
}

// reference instance
let a: A = A()
// reference type
let b: A.Type = A.self

// known types test
print(a.a) // prints "instance member"
print(b.a) // prints "static member"

// specifying generic
let c = B<String>()
let d = B<A>()

// unknown type tests
print(c.unknownTypeScope()) // prints "other type"
print(d.unknownTypeScope()) // prints "static member"

I don't know exactly what is happening under the hood, but it feels to behave as I described.

One main difference from Dart to Swift, is that you don't reference the type by the name only, you gotta use .self. Ex: A.self.

eernstg commented 3 years ago

@lslv1243, sorry, I forgot about this one, here we go:

One substantial difference between Dart and Swift is that Swift static members are inherited, and it is possible to invoke a static member on the object that reifies a class as well as on the object that reifies a subclass thereof. If you use class func rather than static func then you can override the implementation with a new declaration, as long as it has a signature which is a correct override of the one in the superclass.

Dart static members belong to a specific declaration (of a class, mixin, extension, and there may be more variants in the future), and there is no connection between the static members of a class and the static members of its subtypes (subclasses or other subtypes). So there is no requirement that those static members should have a correct override relationship, and static members are always resolved statically.

This means that in Swift we can invoke static members on the object that reifies the class and preserve static typing, but in Dart there is no override constraint and no OO dispatch, and it would be a completely untyped kind of invocation if we were to allow static members to be invoked on instances of Type.

This is true both in the current model with a non-generic type Type, and in a model where the Type for C has type Type<C>, and in a model where that reified type is denoted by C.type and has type C.Type, and D.Type <: C.Type whenever D <: C.

But the Swift model corresponds quite nicely to a programming idiom where we create an association between each Dart class and a separate "type object":

class TypeG<X> {}

class Atype implements TypeG<A> {
  const Atype();
  String get a => "Static member";
}

class A {
  static const type = Atype();
  String get a => "Instance member";
}

class SubAtype implements TypeG<SubA> {
  const SubAtype();
  String get a => "Another static member";
}

class SubA extends A {
  static const type = SubAtype();
  String get a => "Another instance member";
}

const _typeMap = <Type, TypeG>{ 
  A: Atype(),
  SubA: SubAtype(),
};

TypeG<X> typeMap<X>() {
  // The compiler could eliminate the cast because it generates `_typeMap`.
  return _typeMap[X] as TypeG<X>;
}

bool isSubtype<X, Y>() => <X>[] is List<Y>;

class B<T> {
  String unknownTypeScope() {
    if (isSubtype<T, A>()) {
      var type = typeMap<T>() as Atype;
      return type.a;
    } else {
      return "other type";
    }
  }
}

void main() {
  // reference instance
  var a = A();
  // reference type
  var b = A.type;

  // known types test
  print(a.a); // prints "instance member"
  print(b.a); // prints "static member"

  // specifying generic
  var c = B<String>();
  var d = B<A>();

  // unknown type tests
  print(c.unknownTypeScope()); // prints "other type"
  print(d.unknownTypeScope()); // prints "static member"
}
Levi-Lesches commented 3 years ago

I also think that adding contructors/factories/static methods to the target of extensions instead of the extension itself is valuable. Consider the following:

class Person {
  final String name;
  Person({required this.name});
}

extension on Person {
  static Person named(String name) => Person(name: name);
}

void main() {
  Person person = Person.named("John Doe");  // Error: Method not found: 'Person.named'.
}

Which struck me as odd, since the compiler didn't complain when I added the static named method. Took me a while to even consider that the extension was the problem, so I named the extension to NamedPerson and then did NamedPerson.named("John Doe"), which worked. If I'm not mistaken, this means that static methods for unnamed extensions disappear.

I'd be in favor of the conventions above, namely that static methods/factories/constructors can only be added to classes, and not types. That way, I could simply write:

extension on Person {
  factory named(String name) => Person(name: name);
}
// ...
Person john = Person.named("John Doe");

Another point to mention is that, intuitively, extensions should be used the same way the regular API is used. Meaning, users who import my person.dart file shouldn't care if I write methods in Person or NamedPerson -- any Person object can access them. To make static members different breaks this assumption, and leads to unclear behavior.