Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Static extension methods #723

Open
rrousselGit opened this issue Dec 6, 2019 · 122 comments
Open

Static extension methods #723

rrousselGit opened this issue Dec 6, 2019 · 122 comments
Labels
feature Proposed language feature that solves one or more problems static-extensions Issues about the static-extensions feature

Comments

@rrousselGit
Copy link

rrousselGit commented Dec 6, 2019

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 rrousselGit added the feature Proposed language feature that solves one or more problems label Dec 6, 2019
@rrousselGit
Copy link
Author

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
Copy link
Author

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
Copy link
Member

lrhn commented Dec 10, 2019

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
Copy link
Author

rrousselGit commented Dec 10, 2019

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
Copy link

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
Copy link
Member

lrhn commented Jan 2, 2020

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
Copy link
Author

rrousselGit commented Jan 2, 2020

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
Copy link
Author

@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
Copy link
Member

lrhn commented Jan 6, 2020

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
Copy link

ds84182 commented Jan 6, 2020

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
Copy link

leonsenft commented Feb 7, 2020

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
Copy link

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
Copy link

bitsydarel commented Feb 13, 2020

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
Copy link
Member

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
Copy link

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
Copy link
Author

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
Copy link

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
Copy link

jlubeck commented Apr 2, 2020

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

@faustobdls
Copy link

@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
Copy link
Contributor

@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
Copy link

@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 and factory 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
Copy link

listepo commented May 21, 2020

Any news? Very necessary feature

@marcglasberg
Copy link

@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
Copy link
Member

lrhn commented May 24, 2020

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
Copy link

MarvinHannott commented Jul 12, 2020

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
Copy link

omidraha commented Jul 21, 2020

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

@mit-mit mit-mit moved this to Being discussed in Language funnel May 27, 2024
PIG208 pushed a commit to PIG208/zulip-flutter that referenced this issue Aug 8, 2024
Because static extensions are not supported, we cannot create
app_checks.dart for ZulipApp.ready or ZulipApp.scaffoldMessenger.

See: dart-lang/language#723
PIG208 pushed a commit to PIG208/zulip-flutter that referenced this issue Aug 8, 2024
Because static extensions are not supported, we cannot create
app_checks.dart for ZulipApp.ready or ZulipApp.scaffoldMessenger.

See: dart-lang/language#723
PIG208 pushed a commit to PIG208/zulip-flutter that referenced this issue Aug 12, 2024
Because static extensions are not supported, we cannot create
app_checks.dart for ZulipApp.ready or ZulipApp.scaffoldMessenger.

See: https://g ithub.com/dart-lang/language/issues/723
@Levi-Lesches
Copy link

I find it really unintuitive that members defined in an extension apply to the on-type... except static members. When I write extensions, they're designed to be invisible, but defining a static method means someone now needs to know the name of my extension.

@lrhn

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.

So static members in extensions with such an on-type can just be an error? I find static extensions would be really helpful for fromJson constructors/methods, which wouldn't have this issue, and there are enough contexts that would be okay that I think this special case should just be disallowed without affecting everything else.

That's actually not any worse than static extensions. If someone wants to make a static member on a regular extension, and isn't able to because they used an invalid on-type, they'd have to create a new extension with a proper on-type anyway... just like they would have to with static extensions. Making only the invalid cases be an error and allowing everything else just reduces boilerplate and confusion in the common case, while keeping the corner cases the same.

That means no subtyping, because List<int>.fromList(...) would probably not create a list, only an iterable.

That's already how static members work today, so I wouldn't see that as so disruptive.

To avoid breaking how people may be using static extension members today, how about keeping the static members on the extension's namespace, and in the future allowing it also be used in the on-type's namespace?

@eernstg
Copy link
Member

eernstg commented Aug 23, 2024

The proposal about static extensions that I mentioned here is currently being revised, in this PR.

That PR adds two alternative proposals. One of them ('...variant1.md'') is using a new kind of declaration static extension. The other one ('...variant2.md') is a generalization of the extension declarations that we have today. (Note that 'variant2' is in good shape, but 'variant1' is being updated and needs more work.)

@Levi-Lesches wrote:

So static members in extensions with such an on-type can just be an error?

Using variant2, only some extensions have an on-class (or an on-mixin, etc.), and only an extension that has an on-class (etc) is capable of "injecting" static members and constructors into a different declaration (namely the given class/mixin/... declaration which is the on-class/mixin/...).

For other extensions (e.g., the ones whose on-type is a function type), it is still allowed to declare static members and they can still be invoked like today (using E.staticMethod() where E is the name of the extension).

That seems compatible with your proposal above.

It shouldn't actually be a problem to inject static members into a type alias for a type that could not have been the on-class/mixin/... of a static extension, e.g., a function type or a record type. The proposal currently does not include this, but can be added in the future.

Even void Function({int? name1, required bool name2}).myStaticMethod() shouldn't be a problem, semantically, but it is silly, and who knows whether the parser will ever be in good shape again after the introduction of expressions like that? ;-)

That means no subtyping, because List<int>.fromList(...) would probably not create a list, only an iterable.

That's already how static members work today, so I wouldn't see that as so disruptive.

List<int>.fromList(...) would be an invocation of a constructor which is presumably declared in an extension whose on-class is List and whose type arguments can be instantiated to produce the instantiated on-type List<int>. It could be declared as follows (assuming 'variant2' again):

extension E<X> on List<X> {
  factory List.fromList(List<X> from) => ... something ...;
}

void main() {
  var xs = List<int>.fromList([]);
}

I don't see how it could be acceptable for an expression of the form List<int>.fromList(...) where List.fromList resolves to a constructor declaration (anywhere, but in this case it's inside an extension on List), unless the static type of that expression were List<int>. The static type should not be List<num>, it should not be List<Never>, and it should certainly not be Iterable<S> for any S.

This is not technically a requirement, any new kind of expression could evaluate to an object of any type whatsoever, we just need to specify it in that way. However, I expect that it will be highly inconvenient if we "can't trust the type" of an instance creation. That is, all constructor invocations have the static type we can read off of the expression itself, generative constructors return something of exactly that type, and factories return something whose run-time type may also be a proper subtype of the static type (for soundness, it couldn't be an unrelated type). I think we should preserve that property.

class A {}
class B extends A {}

extension AExtension on A {
  A.generative(): this();
  factory A.factory() => B();
} 

void main() {
  print(A.generative().runtimeType); // 'A', not some subtype.
  print(A.factory().runtimeType); // 'B', which is a subtype, but that's OK for a factory.
}

how about keeping the static members on the extension's namespace, and in the future allowing it also be used in the on-type's namespace?

Sounds like a description of the proposals I've mentioned. Being able to call the static members using the extension name as the syntactic receiver is useful because it can resolve a name clash.

extension E1 on int {
  static void foo() {}
  static void bar() {}
}
extension E2 on int {
  static void foo() {}
}

void main() {
  int.foo(); // Compile-time error, ambiguous.
  E1.foo(); E2.foo(); // OK.
  int.bar(); // OK.
}

@Levi-Lesches
Copy link

Awesome! I did see variant1, which motivated my comment, but didn't really look too deeply into variant2. I'd really look forward to seeing variant2 accepted as it seems to me that it would be the simplest approach to users already familiar with extensions, and will avoid confusion for those who are not.

@stan-at-work
Copy link

What do you guys think the status of this issue is? Or what the timeline for resolving this issue might be?

Is this something that will be included in an upcoming release?

Because lately, there has been a lot more activity on this issue, and it's already 5 years old.

@eernstg
Copy link
Member

eernstg commented Aug 26, 2024

lately, there has been a lot more activity

True. No promises, but this topic currently has the attention of the language team. I think it's likely to be accepted in some form (after a lot of discussion, of course).

@TekExplorer
Copy link

I don't like static extension, and just transparently adding static members apparently has issues involving non-class types, let alone suddenly exposing members that hadn't been designed to be on the on type, given current behavior.

What if we reused a keyword elsewhere?

extension on int {
  on static Handler handler = ...;
}
extension on List<int> {
  // Errors - generic instantiations don't support on static extensions
  // Leaves room for future support
  on static Handler handler = ...;
}

gnprice added a commit to PIG208/zulip-flutter that referenced this issue Sep 24, 2024
Also generalize ValueNotifierChecks to ValueListenableChecks.

We can't write checks-extensions for ZulipApp.scaffoldMessenger
or ZulipApp.ready, though, because Dart doesn't have extensions
for static members.  Apparently they're currently working on them:
  dart-lang/language#723

Co-authored-by: Zixuan James Li <zixuan@zulip.com>
@tatumizer
Copy link

@eernstg:
I've been reading your proposal(s) on static extensions, where one phrase caught my eye:

an extension should not have name clashes with its on-declaration.

Then how are you going to inject the methods from the subclass (to enable the shortcut mechanism) avoiding name clashes? Name clashes for static methods are common, and you always have at least one name clash (unnamed constructor), but in general, you can have more. Hence the question.

@TekExplorer
Copy link

@eernstg:
I've been reading your proposal(s) on static extensions, where one phrase caught my eye:

an extension should not have name clashes with its on-declaration.

Then how are you going to inject the methods from the subclass (to enable the shortcut mechanism) avoiding name clashes? Name clashes for static methods are common, and you always have at least one name clash (unnamed constructor), but in general, you can have more. Hence the question.

Static extensions aren't inherited. They would be only on the on type. No super types, no subtypes.

Not until we get static inheritance anyway, then we would apply it to that instead.

@tatumizer
Copy link

tatumizer commented Nov 8, 2024

My question was about injection planned as part of "shortcut" propolsal under discussion.
You answered the question I've never asked (I know the answer) 😄

Consider AlignmentGeometry class. How do we inject the stuff from Alignment to static extension on AlignmentGeometry?
Since AlignmentGeometry has an unnamed constructor, an attempt to inject Alignment(double, double) into the extension will be flagged as an error (according to the proposal). So in the extension, we will have bottomCenter, bottomRight and others, but the constructor Alignment(double, double) will be separated from its brethren and left to its own devices.
I find it unfair and counterintuitive.

And there's never one cockroach! In general, there can be N conflicting definitions. What to do about them?

The approach suggested by the proposal is not consistent with how "normal" extensions work. They allow conflicting definitions (which would be silently ignored, unless you invoke them explicitly using the extension name as a prefix: E.method). The same could be done for the static extensions.

In another thread, I suggested a second namespace (e.g. $) to hold all injected methods. The idea wasn't well-received though. But why? Turns out, the proposal introduces the second namespace anyway! E.g.

static extension AlignmentGeometryExt on AlignmentGeometry {
  // ,,,
}

where AlignmentGeometryExt is exactly a second namespace, which plays no other role than to serve as a disambiguation device. But if there's nothing to disambiguate, then why is it there?

@lrhn
Copy link
Member

lrhn commented Nov 10, 2024

Static extensions are not intended for the person who wrote the original class.
They should just add the statics directly on the class.
There should be no benefit to declaring statics on your own class through a static extension.
(If there is, it suggests a language feature is missing that allows you to do the same thing directly, like declaring a static and instance member with the same name.)

There is no second namespace, there are extensions on that one namespace, just like existing extension members extend the instance interface, and actual instance interface members take precedence.
For staticb extensions, there won't even be a specificity resolution, they either supply or not, and if two static extensions apply, it's a conflict.

If you want to change what an existing constructor on a class does, you have to be the person owning the code. You cannot do that using static extensions. That is simply not a design goal. Static extensions are due extending, not rewriting.
If you own the code you can change it. If the superclass is abstract, you can change the name of the unnamed constructor that subclasses invoke.
If the class is subclassed from outside of the library, it might require a breaking change.
Or you can add a name extension constructor that forwards to an unnamed constructor of a subclass.

(Static extensions likely cannot declared non-redirecting generative constructors, so those all have to be in the original class.)

Also, I have no problem allowing an extension to declare a member with the same base name as a static member of the on class. It just won't be accessible through the on class's static namespace. You can still write ExtensionName.staticMember directly. That's how you disambiguate if there are multiple applicable static extensions too.

@eernstg
Copy link
Member

eernstg commented Nov 10, 2024

@tatumizer wrote, in response to a remark in the static extension proposal here:

an extension should not have name clashes with its on-declaration.

Then how are you going to inject the methods from the subclass (to enable the shortcut mechanism) avoiding name clashes?

The remark about 'should not have name clashes' refers to a recommended warning. In other words, you can have a name clash, and it is not an error, but you may get a warning (depending on what the analyzer team decides to do in this situation). But the point is that you should get that warning in the following case:

class C {
  static void foo() {}
}

static extension EC on C {
  static int get foo => 42; // Warning, `C` already declares a static member named `foo`.
}

The reason why we'd want this warning is that we can't use EC.foo as a static member of C, because C.foo already has a different meaning (and, just like extension instance methods, the static declaration in the class always wins when we have a declaration in a static extension as well). A similar situation arises for two clashing constructors, and for clashes that involve a constructor and a static member. In other words, a name clash simply implies that the injection doesn't happen.

If we want to inject some static members / constructors into a class then we must ensure that they don't have name clashes. This means that we'd need some kind of rename mechanism if we want to inject two declarations with the same name into a common superclass (or, indeed, any class). This is a built-in capability of some mechanisms, including redirecting factories.

For instance, in the standard example where we wish to add constructors from EdgeInsets and from EdgeInsetsDirectional into EdgeInsetsGeometry, the following renamings have been proposed:

static extension EdgeInsetsGeometryExtension on EdgeInsetsGeometry {
  const factory EdgeInsets .fromLTRB = .fromLTRB;
  const factory EdgeInsets .all = .all;
  const factory EdgeInsets .only = .only;
  const factory EdgeInsets .symmetric = .symmetric;

  static const zero = EdgeInsets.only();

  const factory EdgeInsetsDirectional .fromSTEB = .fromSTEB;
  const factory EdgeInsetsDirectional .onlyDirectional = .only;
  const factory EdgeInsetsDirectional .symmetricDirectional = .symmetric;
  const factory EdgeInsetsDirectional .allDirectional = .all;

  static const zeroDirectional = EdgeInsetsDirectional.only();
}

This example assumes that we have #4135 (such that we can omit the formal parameter lists) and #4153 (such that we can specify the return type, and #4144 (such that we can omit the target class name EdgeInsetsGeometry from all the constructor declarations). If we don't have those features then the declaration will be more verbose and less tightly typed, but the core is the same: We can inject some constructors into EdgeInsetsGeometry that we've selected from some other declarations (EdgeInsets and EdgeInsetsDirectional), and then we choose the name of the injected declaration such that there are no name clashes.

Consider AlignmentGeometry class. How do we inject the stuff from Alignment to static extension on AlignmentGeometry?

Does this mean "how can we inject stuff from Alignment into AlignmentGeometry using a static extension"?

static extension on AlignmentGeometry {
  const factory Alignment .someName = .new;
}

We already have a constructor whose name is AlignmentGeometry, so we have to come up with a new name and provide the constructor of Alignment with that new name. So let's pretend that we've chosen AlignmentGeometry.someName. The static extension above will then inject this name into AlignmentGeometry as a factory constructor with return type Alignment, redirecting to the constructor whose name is Alignment.

With some variant of #357, we can then use it like .someName(10.0, 11.0) when the context type is AlignmentGeometry.

It seems quite likely that AlignmentGeometry would be designed differently today, such that the constructor named AlignmentGeometry would be redirecting to Alignment, and you wouldn't have to invent a new name in order to avoid the name clash. The constructor const AlignmentGeometry(); would then be renamed to const AlignmentGeometry.someOtherName(); and subclasses would have to be adjusted. However, the need to save good constructor names in abstract classes only really comes up with .identifier shorthands, so the designers who wrote const AlignmentGeometry(); surely didn't think about this.

I find it unfair and counterintuitive.

Sounds like you'd be much happier if you could simply say in Alignment on the relevant parameters. ;-)

The approach suggested by the proposal is not consistent with how "normal" extensions work. They allow conflicting definitions (which would be silently ignored, unless you invoke them explicitly using the extension name as a prefix: E.method).

There is no difference. The proposals here do allow a static extension to declare a static member or constructor whose name clashes with a declaration in the on class/mixin/etc. It is recommended that the analyzer should emit a warning when there is a name clash, but the name clash is not an error, and the on type wins (just like extension instance members). It is also true that you can invoke declarations with clashing names by using the static extension name itself:

class A {
  static int foo = 1;
}

static extension AE on A {
  static String foo = 'Hello';
}

void main() => AE.foo.substring(1); // OK.

... second namespace ... plays no other role than to serve as a disambiguation device

That is not quite accurate. A crucial property of the name of a static extension is that it allows for the static extension to be imported (or hidden), whereas the "nameless" static extension has a private name (which means that it is local to the declaring library).

@tatumizer
Copy link

tatumizer commented Nov 10, 2024

So let's pretend that we've chosen AlignmentGeometry.someName

Pretending can get us a long way, right until we have to actually choose someName, which is difficult. 😄
But it's not only the problem of unnamed constructor. fromSTEB tells us less than EdgeInsetsDirectional.fromSTEB.
There are also different constants: Alignment.bottomLeft and AlignmentDirectional.bottomStart are not in conflict, and for the compiler, the shortcuts .bottomLeft and .bottomStart are distinct, but for a human reader, it is not clear that one of them is directional.

My guess is that as a general rule, you propose to add the suffix Directional to every conflicting name from *Directional class. As a result, some definitions will look like they are not from *Directional whereas in fact they are. The device looks a bit kludgy to me. I can't see a general concept here. Of course, we could add Directional suffix to everything (not only conflicting names, but all names) we inject from *Directional subclasses, but then .bottomStartDirectional would be redundant for anybody familiar with the meaning of the constant... Turning the namespace into the name suffix still looks kludgy. What's worse: it changes the API for no particular benefit. And it changes it in unpredictable ways: there are 4 pairs of (*, *Directional) classes in Flutter's painting library, but there's the whole universe of other classes (thousands of them) where we can encounter different problems.

I also have issues with the fact that each "borrowed" method now belongs to 3 +1 namespaces. Using all as an example, we can write

EdgeInsets.all(...);
EdgeInsetsGeometry.all(...);
EdgeInsetsGeometryExtension.all(...);
.all(...); // with a context type EdgeInsetsGeometry

Don't get me right wrong: I'm not saying that this is a fault of the design of static extensions. It's just that most of the commenters for some reason assumed (as a self-evident fact) that all the problems of shortcuts can be magically resolved by static extensions. I'm not sure this is the case - and even if it is, it may require a different perspective.

I tried to come up with the alternative: e.g. declare one of the classes as "default", and use the notation $.method to refer to the methods of the class (this eliminates the need to mangle names or inject methods to the namespace where it doesn't belong). (So there won't be any shortcuts for *Directional classes). Maybe someone can suggest a better idea. But at the time of writing, no one seems to be looking for it. 😢

(another option is to admit that *Geometry classes and similar don't lend themselves to shortcuts, and do nothing about them)

@TekExplorer
Copy link

Adding extra syntax for defaults ($) doesn't make much sense. I sort of see what you're saying, but also not really.

If it doesn't work as a default, or doesn't have a reasonable name, then you should just use the "real" one. Be specific when you want something specific.

That said, it's not as if you couldn't namespace things with an object like .directional.thing() if it makes sense to.

API design is not something we should argue about, and we should remember that shoehorning shortcuts where they don't make sense only hurts everyone.

Static typedefs (like a discount nested class) can be a nice synergy feature later on too.

@tatumizer
Copy link

tatumizer commented Nov 11, 2024

Name mangling is a non-starter IMO - it's aethetically ugly, changes the API, etc.
What if we treat $ as a disambiguation device (instead of an indicator of "default implmentation")?
That is $.id means: take id from an extension.
Then we can inject all methods from B to A as is, with their native names. When the user types . in IDE, the IDE shows the suggestions, but substitutes $.id for ALL ids coming from an extension.

It still relies on the notion of "default implementation" (we inject the methods from it - so no "directional" methods there), but ithe latter gets de-emphasized with this treatment (the wording "id is coming from an extension" vs "coming from the default implementation" might be less controversial).

(This entails the syntax .(...) for the constructor of A, and $(...) for B.)

Disambiguation via $. can also be used in "normal" extension methods in place of ugly ExtensionName (unless there are conflicts between extensions themselves)

@TekExplorer
Copy link

That doesn't make much sense though. If you need something on B that doesn't fit on A, just use B. You simply won't know where $ even comes from. At least . Is on the actual context type, which is easy to find. Don't add magic.

Also, changing the API is fine as long as you're smart about it. If the name is too badly mangled, then don't add it, or consider other patterns, like a nested factory ie .thing.constructor()

@tatumizer
Copy link

What "thing"? 😄
E.g. how are you suppposed to call the constructor Alignment(double, double) after injecting it into AlignmentGeometry?
If your logic (from the first paragraph) suggests that it's better to call it like Alignment(0.1, 0,1) then why bother with inventing a "thing"?
I don't quite understand your line of reasoning.
On the one hand, you don't want to add "magic". On the other, you are conjuring up "things". 😄

@eernstg
Copy link
Member

eernstg commented Nov 12, 2024

@tatumizer wrote:

Pretending can get us a long way, right until we have to actually choose someName, which is difficult. 😄

True, naming is hard. But we can't expect any language mechanism to do anything substantial about this.

I think the main issue here is software evolution vs. initial software designs. If shorthands based on the context type had been available when the classes EdgeInsetsGeometry and its subtypes were designed then it seems quite likely that there would have been a strong incentive to populate EdgeInsetsGeometry with a useful and reasonably complete set of constructors (and static members), or at least that it would be possible to do so using static extensions (or some other injection feature).

That wasn't the case and now some really good constructor names in the static namespace of EdgeInsetsGeometry have already been taken. As long as we stick to the design decision that extensions (static or instance) can't shadow declarations in the on class/mixin/etc itself (and I do think that's the appropriate choice), we just can't fix this. That is, unless we accept that there is a need for a breaking change, and we actually rename the constructor in EdgeInsetsGeometry to .someOtherName, and fix all the code that breaks because of this.

Similarly, if the shorthands had been on the table when this hierarchy was designed then we probably wouldn't have had two constructors named .all in the subtypes of EdgeInsetsGeometry.

We simply can't expect to avoid naming issues when the language evolves, there will be situations where a certain approach to naming turns out to be suboptimal later on, when the language adds new features.

Note that it would be possible to have several declarations of static extensions that are injecting constructors into EdgeInsetsGeometry. Developers could import the ones they prefer, for any given library. For instance, if you always use one of the ...Directional classes then you could choose to have a static extension in scope that injects only those constructors into EdgeInsetsGeometry, with good names. The trade-off is, of course, that we could end up with a cacophony of naming schemes, and it might get hard to read what's going on if you're working on several libraries at the same time, and they all use different names for the same constructors, or the same name for different constructors. As usual, developers need to make good choices, and this is not something which can be done at the language level.

I also have issues with the fact that each "borrowed" method now belongs to 3 +1 namespaces. Using all as an example, we can write

EdgeInsets.all(...);
EdgeInsetsGeometry.all(...);
EdgeInsetsGeometryExtension.all(...);
.all(...); // with a context type EdgeInsetsGeometry

That's a good point! However, having EdgeInsetsGeometry.all as well as EdgeInsets.all which are both the same constructor (possibly even with the same return type, modulo #4153) is no worse than the exact same thing using the existing factory constructor redirection mechanism.

EdgeInsetsGeometryExtension.all is different: Direct access to members of an extension (static or instance) is a rare corner case, and it's probably rather easy to recognize it as such because of the long name (that even ends in Extension). That probably doesn't matter, it's no worse than having ListExtensions and similar extension declarations in the name space, even though they are almost never used as names in practice, they just need to be imported with a name such that they can be used implicitly.

The abbreviated one, using .all, builds directly on the one in EdgeInsetsGeometry (if that's the context type). I'm assuming that we are all pretty much supporting the notion that EdgeInsetsGeometry.all(...) means the same thing as .all(...) when the context type is EdgeInsetsGeometry.

you propose to add the suffix Directional to every conflicting name from *Directional class

I wouldn't even begin to try to solve this kind of problem in general. Naming is hard! ;-) .. and we all have to deal with it in concrete ways, in concrete code.

@TekExplorer
Copy link

TekExplorer commented Nov 13, 2024

Naming is out of scope of this issue though, if we only get the benefit in new classes or distant-future breaks, then so be it. (Of course we would also get the benefit for enums and pre-existing static members, which itself can be enough to justify the issue)

We can't expect the feature to be exactly perfect everywhere at the first go can we?

Let the API developers figure out if they want to set certain defaults themselves, and if anyone else wants to add more via static extensions, then all the power to them.

Naming is on them, not the feature.

We should try to remember not to get lost in the sauce, even if the shorthand may not be useful everywhere, it will be useful in enough places, and future API design will only improve that.

Not everything has to fit through the square hole. Not all problems are nails. (When all you got is a hammer(shorthand))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems static-extensions Issues about the static-extensions feature
Projects
Status: Being spec'ed
Development

No branches or pull requests