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 · 104 comments
Open

Static extension methods #723

rrousselGit opened this issue Dec 6, 2019 · 104 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);

@rrousselGit
Copy link
Author

rrousselGit commented Jan 26, 2023

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

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

@AlexVegner
Copy link

AlexVegner commented May 12, 2023

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

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

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

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

final u1 = User(firstName: 'A', lastName: 'V'); 
final (:firstName, :lastName) = u1;
final u2 = User.fromJson({'firstName': 'A', 'lastName': 'V'});
final u3 = (firstName: 'A', lastName: 'V') as User;
assert(u1 == u2);
assert(u1 == u3);
assert(u1 is User);
assert(u1 is ({String? firstName, String? lastName}));
  • And of course will be nice to have static extension for it)
data class Category extends ({String name, int id});

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

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

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

@lrhn
Copy link
Member

lrhn commented May 12, 2023

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

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

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

@AlexVegner
Copy link

AlexVegner commented May 12, 2023

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

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

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

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

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

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

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

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

@AlexVegner
Copy link

I have created separate ferature proposal for it (#3075)

@zhxst
Copy link

zhxst commented Nov 7, 2023

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

@cedvdb
Copy link

cedvdb commented Dec 15, 2023

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

@rrousselGit
Copy link
Author

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

extension on Foo {
  static void fn() {}

  void someExtensionMethod() {
    fn();
  }
}

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

@rrousselGit
Copy link
Author

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

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

@icnahom
Copy link

icnahom commented Dec 15, 2023

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

I want to add static methods or factories to an already existing Flutter widgets. When exposing custom components I would like to expose them as Text.custom("value), for example.

@cedvdb
Copy link

cedvdb commented Dec 16, 2023

@icnahom I was asking about the use case of the current behavior, not the alternative one. Although your example argues for factories, static method would still be useful in the case of asynchronous factories.

@TekExplorer
Copy link

TekExplorer commented Dec 16, 2023

It would be really nice for packages to be able to add custom widget designs to existing widget types
ex: Card.glass() for a glassmorphism style card, without needing to use a custom class. (the custom class would still exist, we the user simply wouldnt need to use it directly)

This would greatly aid in discoverability and readability.

@eernstg
Copy link
Member

eernstg commented Dec 18, 2023

There is a proposal about static extensions which is part of the proposal about implicit constructors.

This proposal would enable the following (I didn't generalize the declaration of fibonacci, it still ignores depth, but that doesn't matter for the overall topic of this issue):

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

void main() {
  List<int>.fibonacci(6); // OK.
  List<String>.fibonacci(6); // Compile-time error: No such constructor.
  List.fibonacci(6); // OK, if we can generalize type inference suitably.
}

The proposal does not support adding constructors to non-classes like function types, which means that the following would be an error:

typedef MyEventHandler = void Function(Event);

static extension MyShortcut on MyEventHandler { // Compile-time error.
  ... // Doesn't matter.
}

However, we would be able to do the following:

extension type MyEventHandler(void Function(Event) it) {
  MyEventHandler.debounced(Duration duration, MyEventHandler handler)
      : this((Event event) { .... });
  void call(Event event) => it.call(event); // Make `MyEventHandler` callable.
}

It is not obvious to me whether it would be useful to allow static members to be injected into function types or other structural types. For instance, we probably can't assume so much about the purpose of a given function type like, say, void Function(String). We could inject static members into that type from anywhere, for any purpose, and those static members could conflict, or we could just see one declaration in some documentation and inadvertently call a completely different declaration that just happens to be imported into the current library. In other words, injecting static members into a structural type seems somewhat dangerous.

In any case, the proposal does not support adding static members to any structural types, but if we really want that then it would presumably not be technically difficult. I'm just not inclined to add it at this time.

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

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

 // Assume that `MyEventHandler` is the extension type shown above.

static extension MyShortcut on MyEventHandler {
  static void somePremadeHandler(Event event) {}
}

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 discussed
Language funnel
Being discussed
Development

No branches or pull requests