Open rrousselGit opened 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
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.
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
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?
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 CustomEvent
s.
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' });
}
}
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.)
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
@tatumizer With such syntax, how would you represent generic extensions?
static extension<T extends num> on List<T> {
factory example(T first) => <T>[first];
}
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.
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).
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());
}
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...
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.
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.
As has been pointed out,
static
is already allowed in extensions, and it creates static methods in the extension namespace, not on theon-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;
}
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.
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.🧐
Definitely agree that the static method should be called from the original class and not the extension class.
@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)
},
);
}
@faustobdls In that case, it seems pretty counterintuitive to me to do what you are proposing for these two reasons:
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.
What should happen when the material Colors
class is updated and now declares members with the same names?
@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
Any news? Very necessary feature
@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.
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?
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;
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);
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.
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});
}
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);
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 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.
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).
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.
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.
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];
}
@tatumizer You can already declare static
methods on extension
s, 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
.
@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.
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;
}
@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'
@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.
@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.
@tatumizer Yes, it does because it would print baz
before and extension
after your proposed change.
@creativecreatorormaybenot
@tatumizer Yes, it does because it would print
baz
before andextension
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)
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 isClassName
orClassName<type args>
, then static declarations of the extension can also be accessed asClassName.staticName
iff the class does not already declare a static member with the same base name asstaticName
, and the extension may also declare factory constructors (the class name used in the factory constructor declaration may be eitherClassName
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.
@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.
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)
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.
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>
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 :)
Any news?
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 😅
@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.
@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
.
@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"
}
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.
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 aDuration
.Ideally, we'd want:
But that is currently not supported. Instead, we have to write:
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
andfactory
keyword inside extensions.Factory
Factories would be able to capture the generic type of the extended type, such that we can write:
Which means we'd be able to do:
But not:
Factories would work on functions and typedefs too (especially after #65):
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:Or
Platform
to add customisWhatever
:which, again, would work on functions and typedefs