-
Notifications
You must be signed in to change notification settings - Fork 207
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
Extension types #2727
Comments
I like the simplicity of this proposal in particular. It has all the semantics expected of a wrapper/newtype and in its current form does not seem to have many caveats. How does this interact with generics? I'm guessing Also, wondering if this could avoid the field. Like: inline class IdNumber on int {
IdNumber(super);
operator <(IdNumber other) => super < other.super;
} This may be breaking though, as it'll start treating |
Exactly.
That would certainly be possible. The use of an However, the main reason why the proposal does not do these things today is that we gave a lot of weight to the value of having an inline class declaration which is as similar to a regular class declaration as possible. For instance, the use of constructors makes it possible to establish the representation object in many different ways, using a mechanism which is well-known (constructors, including redirecting and/or factory constructors). We may very well get rid of the field in a different sense: We are considering adding a new kind of constructor known as a 'primary' constructor. This is simply a syntactic abbreviation that allows us to declare a constructor with a parameter, and implicitly get a final field with the parameter's type. This is similar to the primary constructors of Scala classes: It is part of the header of the declaration, it goes right after the name, and it looks like a declaration of a list of formal parameters. inline class IdNumber(int value) {
bool operator <(IdNumber other) => value < other.value;
} The likely outcome is that primary constructors will be added to Dart, and they will be available on all kinds of declarations with constructors (that's classes and inline classes, for now). However, the design hasn't been finalized (and, of course, at this point we can't promise that Dart will have primary constructors at all), so they aren't available at this point. |
Does this feature replace the previous proposal of views? |
Probably you have already considered it, but I wonder if it could be useful to extend the specification to support also inline classes with multiple fields. |
An inline class with multiple fields cannot be assignable to With records, you can store several values in one object, so an inline class on a record type could behave like an inline class on multiple fields, with an extra indirection. We could consider allowing multiple fields, and then implicitly making the representation type into a record, but then we'd have to define the record structure (all positional in source order, or all named?). |
@purplenoodlesoop wrote:
Yes. The main differences are that the construct has been renamed, and the support for primary constructors has been taken out (such that we can take another iteration on a more general kind of primary constructors and then, if that proposal is accepted, have them on all kinds of classes). |
@eernstg It seems that this new proposal doesn't have the possibility to |
True. I'm pushing for an extension to the inline class feature to do those things. It won't be part of the initial version of this feature, though. |
I'm curious whether a future version of this feature might include some standardized support for enforcing that the type is a subset of all available values? For example, the Even without a formal ranged value feature, it would be nice to have a convention for asserting correctness of a value, for example, having something like |
The current version definitely do not provide any such guarantee. An alternative where we retain the inline-class type at runtime, so the type I don't think it's impossible. I think making That'd be a feature with much different performance tradeoffs, and would need deep hooks into the runtime system in order to have types (as sets of values) that are detached from our existing types (runtime types of objects). |
@timsneath wrote:
We could choose to use a different run-time representation of an inline type (so the representation of We could then make I proposed a long time ago that we should create a connection between casts and execution of user-written code (so It would probably be possible to maintain a discipline where every value of an inline type is known to have been vetted by calling one of the constructors of the inline type, and similarly it might be possible to prevent that the representation object leaks out (with its "original" type). So there are several things we could do, but we haven't explored this direction in the design very intensively. With the proposal that has been accepted, the run-time representation of an inline type is exactly the same as the run-time representation of the underlying representation type (for instance I believe it would be possible to introduce a variant of the inline class feature which uses a distinct representation of the type at run time and gives us some extra guarantees. We could return to that idea at some point in the future (or perhaps not, if it doesn't get enough support from all the stakeholders ;-). |
Thanks to both of you. Yeah, the feature you describe sounds pretty exciting. I guess I'm wondering whether the current proposal leads us to having two slightly different solutions to the same use case: inline classes and type aliases. In both cases, the type is erased at runtime; the former is available at compile-time. I'm very much speaking as a layperson here, so my comments don't have a ton of weight. But if we can only have two of the three (erased at compile/runtime, hybrid, distinct at compile/runtime), would we want to pick the two that have the greatest distinction in usage scenarios, to maximize their value? Inline classes seem to make type aliases redundant, at least for the things I use them for today. |
Both type aliases and inline classes can be directly mapped to Haskell's Type synonyms and Newtypes, I think I even saw direct references in the feature specifications. I think both have their uses – typedefs carry a semantic value when inline classes explicitly offer new functionality. I see it as two separate use cases. |
@timsneath, I agree with @purplenoodlesoop that the two constructs are different enough to make them relevant to different use cases. Here's a bit more about what those use cases could be: An inline class allows us to perform a replacement of the set of members available on an existing object (a partial or complete replacement as needed, except members of So we can make a decision to treat a given object in a customized manner—because that's convenient; or because we want to protect ourselves from doing things to that object which are outside the boundary of the inline class interface; or because we want to raise a wall between different objects with the same representation type, so that we don't confuse objects that are intended to mean different things (e.g., A typedef makes absolutely no difference for the interface, and it doesn't prevent assignment from the typedef name to the denoted type, or vice versa. On the other hand, we can use a typedef as a simple type-level function, and this makes it possible to simplify code that needs to pass multiple type arguments in some situations. Here is an example where we also use the typedef'd name in an instance creation: class Pair<X, Y> {
final X x;
final Y y;
Pair(this.x, this.y);
}
typedef FunPair<X, Y, Z> = Pair<Y Function(X), Z Function(Y)>;
void main() {
var p = FunPair((String s) => s.length, (i) => [i]);
print(p.runtimeType);
// 'Pair<(String) => int, (int) => List<int>>'.
}
In the first line of I think this illustrates that typedefs and inline classes can at least be used in ways that are quite different, and my gut feeling is that there won't be that many situations where we could use one or we could use the other, and there's real doubt about which one is better. |
This is a really helpful explanation, thanks @eernstg! (We should make sure that we add something like this to the documentation when we get to that point...) |
I could be missing a bunch of aspects to this, but I think given Dart's direction towards a sound language, it's worth exploring how much the soundness can be preserved with this feature. Specifically, the most useful soundness guarantee would be during the One issue we'd want to avoid is reintroduce the wrapper object problem for containers, since to get a type-safe conversion from This is where having inline class InlineList<F, inline T on F> {
InlineList(this._it);
final List<F> _it;
T operator[] (int index) => _it[index] as T;
List<T> cast() => List.generate(_it.length, (i) => _it[index] as T);
} Here Now we can safely view a typedef SocialSecurityNumberList = InlineList<int, SocialSecurityNumber>;
void main() {
final rawList = <int>[1, 2, 3, 4, 5];
final ssnList = rawList as SocialSecurityNumberList;
print(ssnList[3]); // SocialSecurityNumber constructor invoked here
} The following key properties are preserved:
I'm not sure what the issue is with hot reload. Object representations are still the same. If the issue is with soundness, then for hot reload it can be relaxed. Hot reload already has type related issues. After all, all we do is validate the permitted value spaces. This is already an issue with hot reload. Consider expression:
It extracts odd numbers out of a list, and potentially stores it somewhere in the app's state. Now what happens when you change it to:
Then hot reload. Any new invocations will produce even numbers. However, all existing outputs of this function stored in the app state will continue holding onto odd numbers. All bets are off at this point, and you have to restart the app (luckily there's hot-restart). So I think it's totally reasonable for hot reload not preserve that level of soundness. |
The It cannot work with the current design. When we completely erase the inline class at runtime, the Even if we know that the In short: You cannot abstract over inline classes, because abstracting over types is done using type variables, which only get their binding at runtime, and inline classes do not exist at runtime. We could make all inline classes automatically subtype a platform type That will allow one to enforce that a static type is an inline class type, and be used to bound type parameters like suggested above. (No need for special syntax like We can make So, basically, the current inline class design does not provide a way to guard a type against some values of the representation type, no way to create subsets of types. Anything which requires calling a validation operation at runtime is impossible to enforce, because we can skirt it using the completely unsafe The only security you get against using an incompatible If your program contains no |
Thanks for the detailed explanation! Yeah, this would need extra RTTI to work.
Cool. If we require explicit casts, then maybe this is safe enough as is. Anyway, looking forward to trying this out! This is my favorite upcoming language feature so far 👍 |
Is this feature will be shipped in Dart 3.0? It seems perfect to create value objects by the inline class. |
It is not part of 3.0. |
@yjbanov wrote:
Note that we had some discussions about a feature which was motivated by exactly this line of thinking: In an early proposal for the mechanism which is now known as inline classes, there is a section describing 'Protected extension types'. The idea is that a protected extension type is given a separate representation at run time when used as a part of a composite type (that is, as a type argument or as a parameter/return type of a function type). This means that we can distinguish a The main element of this feature is that an inline class can be protected inline class SocialSecurityNumber {
final int value;
SocialSecurityNumber(this._value); // Implicit body: { if (!isValid) throw SomeError(...); }
bool get isValid => ...; // Some validation.
}
void main() {
var ssn = SocialSecurityNumber(42); // Let's just assume that the validation succeeds.
// Single object.
int i = ssn.value; // The inline class can always choose to leak the value.
i = ssn as int; // We can also force the conversion: Succeeds at run time.
ssn = i as SocialSecurityNumber; // Will run `isValid`.
// Higher-order cases.
List<int> xs = [42];
List<SocialSecurityNumber> ys = xs; // Compile-time error.
ys = xs as List<SocialSecurityNumber>; // Throws at run time.
xs.forEach((i) => ys.add(i as SocialSecurityNumber)); // Runs `isValid` on each element.
void f<Y>(List xs, List<Y> ys) {
xs.forEach((x) => ys.add(x as Y)); // Runs `Y.isValid` on `x` when `Y` is a protected inline type.
}
f(xs, <SocialSecurityNumber>[]); // OK.
f([41], <SocialSecurityNumber>[]); // Throws if 41 isn't valid.
} This mechanism would rely on changing the semantics of performing a dynamic type check. This is the action taken by evaluation of The fact that the invocation of Of course, if This would work also in the case where the source code has a type variable We could express the inline class InlineList<X, I> {
final List<X> _it;
InlineList(this._it);
I operator [](int index) => _it[index] as I; // Runs `isValid` if `I` is protected.
List<I> cast() => List.generate(_it.length, (i) => _it[index] as I);
} The whole idea about protected inline classes was discussed rather intensively for a while, e.g., in #1466, but it did not get sufficient support from the rest of the language team. Surely there would still be a need to sort out some details, and in particular it would be crucial that it is a pay-as-you-go mechanism, not an added cost for every dynamic type check. |
I totally agree that it should be pay-as-you-go. The zero-cost abstraction is the main feature of this. Otherwise, the current classes would be just fine. The kind of safety I'm looking for is mostly development-time safety, where performance is not as important as ability to catch bugs. There's not much recovery to do if But yeah, if this makes the language too complex, then this is fine as specified. |
Silly idea: We could have a special variant of inline classes: When assertions are not enabled it would be a plain inline class. This means that List<I> castList<X, I>(List<X> xs) {
if (X == I) return xs as List<I>;
var ys = <I>[];
for (var x in xs) ys.add(x as I);
} During a production execution we would have It is a silly idea, of course, because we would make production execution substantially different from development execution (some types are different in one mode and identical in another), and that is always a serious source of difficulties, e.g., because some bugs occur in one mode and not in the other. But it's still kind of fun. ;-) |
There's an incorrect statement in the issue description which could lead to some confusion: The issue description says:
That seems to imply that extension types can implement any type. However, extension types can only implement types that are supertypes of the representation type.
|
It's probably too late to bring this up, but the fact that the extension type and the original one are indistinguishable at runtime is very confusing. I was hoping that extension types could solve the problem of variable shadowing in Flutter.
I wanted to get rid of this workaround since the very moment I implemented it, so my first instinct, after learning extension types are available under the experimental flag, was to try this: extension type Country(String name) {}
extension type City(String name) {}
provide(Country('US'));
provide(City('NY'));
//...
consume<City>() // 'NY' – ok
consume<Country>() // 'NY' – Country is shadowed by City I understand all the benefits of having zero overhead for extension types, but not being able to distinguish between an extension type and an original one makes the whole feature less usable. Is there a way to have the same level of overhead and performance but add a way to distinguish types? (e.g. have a view of a |
There is pretty much no chance of adding runtime overhead to extension types, because then it would be a different feature. What you're asking for is just small classes, class Country {
final String name;
const Country(this.name);
} Shorter syntax for that is possible, for example the "primary constructor" proposal which would make it abbreviable to: class const Country(final String name); Alternatively, you can use extension types to hide the tag-record hack: extension type const Tag._((Symbol?, Object?) _) {
const Tag(Object? key) : this._((null, key));
}
extension type const City._((Symbol, String) _) implements Tag {
const City(String name) : this._((#_city, name));
}
extension type const Country._((Symbol, String) _) implements Tag {
const Country(String name) : this._((#_country, name));
} so you can do: provide(City("NY'), ...);
provide(Country("US"), ...);
provide(Tag(SomethingUnique()), ...);
...
var city = consume(City("NY"));
var country = consume(City("US")); and |
Primary constructor solves the "less bolierplate" part of it, but has an overhead of creating a wrapper object.
This looks interesting, but given these tags should be declared by user, it looks even more hacky :) (extension type itself). Possibly could be solved with macros I was also thinking about something like this: abstract class Kind<T> {}
class Country extends Kind<String> {}
class City extends Kind<String> {} which could then theoretically be used as provide<City>("NY");
provide<Country>("US"); but Dart can't infer a generic type unless specified like this: provide<T, K extends Kind<T>>(T); which makes the example above unnecessarily verbose provide<String, City>("NY");
// ^^^^^^ we already know that City extends Kind<String> Maybe there is a way to allow "optional" generic type provide<K extends Kind<T>, [T]>(T); or have a keyword to enforce type inference of a generic type provide<K extends Kind<infer T>>(T); |
If you don't create a wrapper object, you get the current extension types, which are indistinguishable from the original object. (The possible alternative would be "fat pointers" that remember the representation object and the extension types, uses two memory slots for it, but ensures that they are always inlined. That's basically just an always-inlined wrapper object. And it's fundamentally what Rust traits do, and if we wanted to do something like that, we should do it properly, not as a way to add reified types to extension types. They could probably replace extensions, extension types, and possibly mixins.) |
Could #3024 be generalized to that? |
@lrhn wrote:
I wanted to comment on this topic at some point, but apparently never got around to doing it. Before now. ;-) I think it's important to note that Rust has no subtyping. See, for example the introduction to the section about subtyping and variance in the Rust Reference.
As it says, the only subtype relation that exists if we ignore lifetimes is Rust uses coercions in a number of situations where it might be tempting (from an OO point of view) to assume that the operation is a plain assignment of a reference value to a storage location such as a variable or a formal parameter (this kind of assignment would rely on subtyping—which tells us that the actual semantics must be different, because we don't have subtyping). What really happens with dynamically resolved trait member invocations is that an entity of a type In Dart, this corresponds to the following: abstract class MyTrait {
void aTraitMethod();
void anotherTraitMethod();
}
class MyTraitForT implements MyTrait {
final T t;
MyTraitForT(this.t);
void aTraitMethod() {...}
void anotherTraitMethod() {...}
}
class T {...}
void main() {
var t = T();
MyTrait myTrait = MyTraitForT(t);
myTrait.aTraitMethod();
} The other common case is when the invocation of the trait member occurs in a situation where the type of the underlying entity is known. That is, the trait member implementation is resolved statically. In Dart, this corresponds to the following: extension type MyTraitExtension(T it) implements T {
void aTraitMethod() {...}
void anotherTraitMethod() {...}
}
void main() {
var t = T();
var myTrait = MyTraitExtension(t);
myTrait.aTraitMethod();
} The extension type has Some earlier versions of the extension type proposal had support for "boxing" an extension type. That is, the extension type could be used with statically resolved member invocations (just like the extension types that we actually have in the upcoming release), and then there was a reified version as well. That is, the You'd go from the unboxed (statically resolved) version to the boxed version by invoking In the example above, the declaration It wouldn't be hard to reintroduce some variant of the boxing feature, but it does give rise to some distinctions that we don't have with the current extension type mechanism. Let
The main reason why we don't support boxing of extension types is that there are many questions of this nature, and no obviously optimal answers. Dart (and other OO languages) offer a huge amount of flexibility based on subtyping, and that makes it a lot harder to know exactly when and how to perform coercions (and we probably don't want implicit coercions to occur all over the place, especially if they create pervasive identity confusion). Rust omits much of the flexibility by removing subtyping; this makes it possible for Rust to use coercions to make it seem like a given datatype supports the methods declared directly for that datatype as well as a bunch of traits members using compile-time resolution, and also the trait members using run-time dispatch. However, this is a non-trivial trade off. Consider, for example, this stackoverflow question illustrating how difficult it is to compare two trait objects for equality. My conclusion is that Rust is an extremely interesting language with meaningful and well-orchestrated elements, but also that OO languages (and in particular: Dart) have chosen a very different set of trade offs, and the OO side does get a large amount of expressive power, flexibility, and encapsulation, in return for knowing less about the run-time situation at compile time. In other words, Rust traits are great for Rust, but it doesn't make sense to say that we should add them to Dart, because they won't fit, and we don't necessarily want to make the (deep and radical) changes to Dart that would make them fit. ;-) |
Extension types launched in Dart 3.3 🥳 |
Documentation available here: https://dart.dev/language/extension-types |
I find the documentation on redeclaring a bit confusing. Why can't I do this ? import 'package:meta/meta.dart';
void main() {
final Seats remainingSeats = Seats(3) - Seats(1);
}
extension type const Seats(int _value) implements int {
// ...
@redeclare
Seats operator +(Seats other) => Seats(_value + other._value);
@redeclare
Seats operator -(Seats other) => Seats(_value - other._value);
}
|
@cedvdb, please create a new issue for a new topic: This issue was concerned with adding the extension type feature to the language. All the details about how this feature works or should work are being handled in other issues. For issues dealing with the implementation of the feature: Please use the SDK repository, For issues dealing with the language design that is now known as extension types: Check out the extension-types topic as well as extension-types-later. |
That said, I can't see why you wouldn't be able to do as shown here. The example compiles and runs just fine (using a fresh SDK, a slightly older one, 3.4.0-140.0.dev, and the Stable channel of DartDev), and the [Edit: OK, tried DartDev one more time, and this time I got the error. So @lrhn's comment is the relevant one: This behavior has been fixed and will be in a released version soon, just not yet.] |
As Erik says, the code compiles and runs. There is a bug in the analyzer in the 3.3.0 release, which appears to use the special typing rules for |
How to use assert with extension type? |
You might use a named private constructor in the extension type shortand and declare a public constructor as usual: extension type const Ratio._(num value) implements num {
const Ratio(this.value)
: assert(value > 0, 'Value must be positive, non-zero');
} |
@eernstg: have you considered implementing union types as a special kind of extension types?
The type is not reified, so the runtime is not affected. |
https://github.com/eernstg/extension_type_unions I'd like to have implicit constructors (such that we can inject each operand of the union into the union type without adding any syntax, we'd just use the context type), but it is possible to get rather close. |
[Edit, mit-mit, Feb 2024 This feature launched in Dart 3.3; we have docs here.]
[Edit, eernstg: As of July 2023, the feature has been renamed to 'extension types'. Dec 2023: Adjusted the declaration of
IdNumber
—that example was a compile-time error with the current specification.][Edit, mit: Updated the text here too; see the history for the previous Inline Class content]
An extension type wraps an existing type into a new static type. It does so without the overhead of a traditional class.
Extension types must specify a single variable using a new primary constructor syntax. This variable is the representation type being wrapped. In the following example the extension type
Foo
has the representation typeint
.Extension types are entirely static. This means there is no additional memory consumption compared to using the representation type directly.
Invocation of a member (e.g. a function) on an extension type is resolved at compile-time, based on the static type of the receiver, and thus allows a compiler to inline the invocation making the extension type abstraction zero-cost. This makes extension types great for cases where no overhead is required (aka zero-cost wrappers).
Note that unlike wrapper creates with classes, extension types do not exist at runtime, and thus have the underlying representation type as their runtime type.
Extension types can declare a subtype relation to other types using
implements <type>
. For soundness,implements T
whereT
is a non-extension type is only allowed if the representation type is a subtype ofT
. For example, we could haveimplements num
in the declaration ofIdNumber
below, but notimplements String
, becauseint
is a subtype ofnum
, but it is not a subtype ofString
.Example
Specification
Please see the feature specification for more details.
This feature realizes a number of requests including #1474, #40
Experiment
A preview of this feature is available under the experiment flag
inline-class
.Implementation tracking
See dart-lang/sdk#52684
The text was updated successfully, but these errors were encountered: