-
Notifications
You must be signed in to change notification settings - Fork 205
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 Types #42
Comments
wouldn't show/hide be enough for that already? |
It should definitely be possible to hide extension methods ( |
That's one of the things I like about #43 because it's more explicit
I haven't seen anything in the proposal that would hint at that.
also dependent on being able to use prefixes This extension types approach seems to introduce a lot of new concepts not too easy to understand. |
I personally think extension types introduce fewer concepts than extension methods. The methods are the same, but the resolution process is much simpler. (Especially if we assume that we already have type aliases anyway, like I have added text to #41 say that there is potential conflict between instance members and static extension methods, and that I think the sanest way to resolve it is to always pick the extension method. And that prefixes do nothing. There is no syntactic place to use them. |
I like the thought of static extension types more than static extension methods and Static extension methodsStatic extension methods in Kotlin work great because of explicit imports AND function overloads. But a large problem is discoverability, and an even larger problem is libraries that feel that its necessary to add extension methods to all types, which gets annoying during auto completion. Pipeline operatorWhile I see the pipeline operator being used for cleanly composing operations, I don't see it as a clean way to add extended functionality to an object, and it's limiting in certain aspects (for instance, how do I define an operator?). Static extension typesReminds me of Kotlin's |
I think this a major flaw, unfortunately. In your motivating example, you say:
But, unless you control HelperThing helper = foo.something;
helper.somethingElse; That's even worse than using a top level function. You could maybe use (foo.something as HelperThing).somethingElse; But, again, it's not clear that that's an improvement over a function. Method chains are incredibly common, and this problem area is directly about allowing more operations to use method chain syntax. So an extension method system that doesn't let you hang off an existing method chain that calls methods you don't control is going to be really limited. When I was programming in C#, extension methods on IEnumerable were some of the most common ones I defined (example). It would have been maddening if I couldn't call those methods on the result of other methods that return IEnumerable, including the many higher-order sequence transformation methods. |
I think this is a partial (but not complete as @munificent observes) solution for #40, but I don't think #40 is the main (or at least not the only) problem that this mechanism would target. The main thing this gives you is a way of producing:
In other words, I don't think there's necessarily anything (or much) that you can do with this that you can't do with delegation already in Dart. But it gives you a much more convenient syntax for it, and a much more efficient (lightweight) representation. In turn, you give up various capabilities that you would get with the class wrapper/delegation approach:
|
I actually do think (foo.something as HelperThing).somethingElse; is better than the static helper function. It preserves "flow-order", which is also the thought-order I use when understanding what is going on. I've added the "needs a cast" caveat to the initial post. |
I don't prefer this for 2 reasons:
|
This looks close to Golang's defined types but without the same level of utility. Go's defined types get the efficiency from compact object representation and create a new namespace for adding extra methods on top of the underlying types, but they are still distinct from their underlying types (e.g. you can do a runtime type check I think #41 is a better solution for #40. However, I wouldn't say "no" to defined types in Dart, for example for units, such as pixel, meter, Celsius, without the overhead of wrapping objects within objects. |
For what it's worth: in JS interop, I can see both #41 and this proposal (#42) being very useful. What excites me about this proposal is the As others noted, I feel like the two proposals attack two different and valid use cases. Extension members are perfect for adding helpers, and they're great for having an interface (e.g. Iterable) and getting lots of useful functionality "for free" in an extensible way. They're also easy to use: no need to change static types around, break up method chains, or learn new type names and how they relate to each other. Just import the extensions into your library, and you're good to go! Extension types seem excellent for reshaping APIs. That's one thing extension methods don't do well: they can only add things. With this proposal you can remove and rename things too. BTW, one thing I don't especially like: (foo.something as HelperThing).somethingElse; When I see |
Instead of a cast, what about a constructor, like Kotlin's inline classes? From Kotlin's docs on Inline Classes: inline class Name(val s: String) {
val length: Int
get() = s.length
fun greet() {
println("Hello, $s")
}
}
fun main() {
val name = Name("Kotlin")
name.greet() // method `greet` is called as a static method
println(name.length) // property getter is called as a static method
} Makes it easier to convert actual uses of "extension classes" to extension types in the Future. |
I am actually considering constructors (and perhaps even deconstructors) for extension types. Extension types are new nominal types with the same underlying implementation as an existing type. That is both simple and useful. You can change only the static type of a variable and get all the extra features of However, it does not allow you to restrict assignability or do any conversion. So, we can also allow you to declare "constructors" on the extension type. Those are necessarily factory constructors, and they must return the representation type. Example: class Point<T> { final T x, y; const Point(this.x, this.y); }
typedef Complex on Point<num> {
Complex(num r, num c) => Point(r, c);
Complex.angular(num theta, num r) => Point(r * cos(theta), r * sin(theta));
num get r => super.x;
num get c => super.y;
Complex operator +(Complex other) => Complex(r + other.r, c + other.c);
Complex operator *(Complex other) => Complex(r * other.r - c * other.c, r * other.c + c * other.r);
Point<num> toPoint() => super;
} If an extension type has a constructor, then there is no automatic assignability in either direction. You always have to add use the constructors to create an "instance" of the extension type. The constructor can then validate inputs or convert them. Is it too strict to disallow assignment if there is any constructor? What about destructing/destructuring/projecting the representation back out then? If we allow assignability in one direction, it's hard to not allow it in the other direction. I do want the simple assignability. I also do want the constructors. I'm not sure they should be mutually exclusive. Maybe this ability should not be guided by constructors, but by a manual flag: sealed typedef Complex on Point<num> { .... } means there is not assignability between WDYT? |
I like the idea of sealed typedef FNV32Hash on int {
FNV32Hash.fromRaw(int hash) => hash & 0xFFFFFFFF;
FNV32Hash.of(String str) => ...;
int get raw => super;
// Interesting issue, is this valid? Can you override Object methods?
String toString() => "FNV32Hash: " + super.toRadixString(16).padLeft('0', 8);
}
sealed typedef ClassKey on int {
ClassKey.fromRaw(int hash) => hash & 0xFFFFFFFF;
ClassKey.fromFNV32(FNV32Hash hash) => hash.raw;
ClassKey.of(String str) => FNV32Hash.of(str).raw;
int get raw => super;
String toString() => "ClassKey: " + super.toRadixString(16).padLeft('0', 8);
}
// C+P ClassKey for CollectionKey, etc. Several questions I can think of:
|
I'm not sure FWIW, modern Pascal variants support this kind of thing specifically for what we would call "value types", like type
TFoo = TBar; // essentially an alias, useful for generics and the like
TBaz = type TBar; // a new type that happens to be identical to the other This is useful e.g. to have a different type for "meters" and for "inches", or for "celsius" and for "fahrenheit", so that you can't accidentally assign one to the other. (You can still cast from one to the other, which is essentially free since it's just a way to disable the type checking.) |
Yes, this is something we've been thinking about. One of my goals for this class of features is to provide some way to support close to zero-cost abstractions, which means no boxing. That is, I can say something like: typedef Idx on int {... } and be confident that the representation of
|
I don't quite see how this is related to #40. More specifically the: helper(foo.something).somethingElse as this proposal requires a casting, which makes the syntax worse. This ultimately is syntax sugar over wrapper classes, such as this one: class Foo implements Bar {
final Bar _wrapped;
Foo(int value): _wrapped = Bar(value);
int get value => _wrapped.value;
void newFeature() {
// TODO: do something cool
}
} Which can already be solved using mixins shorthand syntax: mixin _Foo on Bar {
void newFeature() {
// TODO: do something cool
}
}
class Foo = Bar with _Foo; Maybe a good middleground would be to enhance that syntax sugar with the ability to override some things without having to rewrite the entire class: class Foo = Bar with Mixin {
@override
void methodOverrideExample() { }
} |
This is indeed syntactic sugar for something equivalent to wrapper classes. The idea is that it is based entirely on static types, so you don't need to allocate a wrapper object. That makes it possible to implement it as a low-cost aliasing operation rather than an actual wrapper class. Even if it is implemented as a wrapper class, it provides a way to easily define such a wrapper class without having to forward all the methods. typedef Lister<E> extends List<E> {
Lister<E> repeat(int count) => List<E>.generate(this.length * count, (i) => this[i%this.length]);
// Forwards all List methods to super.
} It is, however, not the same as a mixin. A mixin creates a new class with the original class as super-class. It doesn't allow you to wrap existing objects, only create new objects. Wrappers allow you to dynamically wrap an existing object that was created by someone oblivious to your type. |
The more I think about this, it seems to me that there are two different features to consider: Transparent Extension Types: A type alias for another type wrt. subtyping, but with different members. Only exists at compile-time, it is entirely static. Basically we introduce an extension type, which is not an interface type. We treat member access on that type differently from member lookups on interface types. The extension type is otherwise equivalent to its We could even use the existing extension method definition as the transparent extension type definition. The extension name becomes the extension type name. Opaque Extension Types: Introduces a new type which is completely unrelated to all other types except (Definitely some unclear issues here). |
What is the benefit of transparency extension types? |
I guess, |
@Cat-sushi wrote:
Transparent extension types allow us to impose a special discipline on a given object graph. The typical example could be a You cannot "declare a subtype" of such an object graph that contains a similar level of discipline. You could transform the whole object graph to a tree of typed Dart objects, but that's much more costly in terms of time and space. |
You can't implement You can't extend Creating a subtype of extension EvenInt on int {
EvenInt(int value) => value.isEven ? value : throw ArgumentError.value(value, "value", "Is not even");
EvenInt._unverified(int value) => value;
EvenInt operator +(EvenInt other) => EvenInt._unverified(this + other.toInt());
EvenInt operator -(EvenInt other) => EvenInt._unverified(this - other.toInt());
EvenInt operator *(EvenInt other) => EvenInt._unverified(this * other.toInt());
int toInt() => this;
} This could be a valid subtype of |
I think this is covered by extension types. |
Solution for #40. This is not a full proposal, just a primer for discussion.
Scoped static extension methods (#41) are useful, but risk naming conflicts and makes it hard to see where a function comes from.
An alternative is to not add the methods directly on the existing type, but instead introduce a new type alias for the original type, and then only attach the new methods to that alias.
The alias can expose the original object's members, or it can hide them and expose other methods instead.
Example:
This declares a new type alias
Stringy
which extends the typeString
with a new methoddouble
.Any
String
value can be cast to and fromStringy
.Example:
Just like extension methods, the methods are really static, just with a special binding for
this
.The
Stringy
type does not exist at run-time. Effectively it's like normal static extension methods that are only declared on static-only type aliases.This prevents naming conflict, the user picks the extensions that they want on a case-by-case basis. It's entirely opt-in, there is no effect-at-a-distance where one import can add extension methods to another import's class - you have to choose to use the extension.
If the static extension type wants to replace functionality, it can choose to not expose the original object's API:
This provides a different view on an existing object. It uses
super
to access the member of the adapted object, rather the vector's own API.Maybe we can add multiple
on
constraints, like on themixin
declaration. Then using this type will be similar to using a statically resolved mixin application.It's possible to add constructors like:
(which would disable the default assignability from Point to Vector?)
Again, we the typedef can be generic:
and if we can deconstruct types, it might also apply here:
giving the extension methods access to the reified generic type argument of the receiver
Static extension types have a drawback compared to extension methods like #41.
In a chain of operations, you need to do an explicit cast to enable the new functionality, e.g.:
where the extension method can be put directly on the type returned by
foo.something
.Static extension types like these may be useful as simple data types if Dart adds simple tuple types. Then we could declare something like:
This is a simple structural data-type with extra functionality, implemented on top of tuples and static extension types.
The text was updated successfully, but these errors were encountered: