language icon indicating copy to clipboard operation
language copied to clipboard

Extension types

Open mit-mit opened this issue 1 year ago • 77 comments

[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 type int.

extension type Foo(int i) { // A new type that wraps int.
 
  void function1() {
    print('my value is $i');
  }
}

main() {
  final foo = Foo(42);
  foo.function1(); // Prints 'my value is 42'
}

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 where T is a non-extension type is only allowed if the representation type is a subtype of T. For example, we could have implements num in the declaration of IdNumber below, but not implements String, because int is a subtype of num, but it is not a subtype of String.

Example

// Create a type `IdNumber` which has `int` as the underlying representation.
extension type IdNumber(int i) {
  // Implement the less-than operator; smaller means assigned before.
  bool operator <(IdNumber other) => i < other.i;

  // Implement the Comparable<IdNumber> contract.
  int compareTo(IdNumber other) => this.i - other.i;

  // Verify that the IdNumber is allocated to a person of given age.
  bool verify({required int age}) => true; // TODO: Implement.
}

class Foo implements Comparable<Foo> {
  @override
  int compareTo(Foo other) => 1;
}

void main() {
  int myId = 42424242; // Storing an id as a regular int.
  myId = myId + 10;    // Allowed; myId is just a regular int.

  var safeId = IdNumber(20004242); // Storing an id using IdNumber.
  myId = safeId + 10; // Compile-time error, IdNumber has no operator `+`.
  myId = safeId;      // Compile-time error, type mismatch.

  print(safeId.verify(age: 22)); // Prints true; age 22 matches.
  myId = safeId as int; // Extension types support type casting.
  print(safeId.i);      // Prints 20004242; the representation value can be read.

  final ids = [IdNumber(20001), IdNumber(200042), IdNumber(200017)];
  ids.sort();
  print(ids);

  dynamic otherId = safeId;
  print(otherId.i); // Causes runtime error: extension types are entirely static, ..
  // .. the static type is `dynamic`, and the dynamic type of `otherId` has no `i`.
}

Specification

Please see the feature specification for more details.

This feature realizes a number of requests including https://github.com/dart-lang/language/issues/1474, https://github.com/dart-lang/language/issues/40

Experiment

A preview of this feature is available under the experiment flag inline-class.

Implementation tracking

See https://github.com/dart-lang/sdk/issues/52684

mit-mit avatar Dec 16 '22 12:12 mit-mit

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 <int>[] as List<IdNumber> succeeds but assignment without a cast is a compile time error. At runtime the cast is still a no-op.

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 super as an expression and not as a receiver.

ds84182 avatar Dec 19 '22 21:12 ds84182

I'm guessing <int>[] as List<IdNumber> succeeds but assignment without a cast is a compile time error. At runtime the cast is still a no-op.

Exactly.

wondering if this could avoid the field

That would certainly be possible. The use of an on clause was part of some earlier proposals for this feature, and we also had some proposals for using super to denote "the on object" (now known as the representation object). By the way, it shouldn't create any big issues to treat super as an expression.

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.

eernstg avatar Dec 19 '22 22:12 eernstg

Does this feature replace the previous proposal of views?

purplenoodlesoop avatar Dec 30 '22 12:12 purplenoodlesoop

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. Similarly to an inline class with a single field, no additional runtime instance would exist, but only the values of the fields would exist at runtime. A variable which static type is an inline class having n fields, would correspond at runtime to a set of n (hidden) variables (one per field of the inline class). The effect would be simular to an inline class with a single field that wraps an immutable record, but without the need to have the immutable record and without the disadvantage of having the actual fields nested in another field. I guess that inline classes with multiple fields could be useful to create efficient code that imposes a discipline on the usage of multiple objects created by third party libraries (e.g. to relate objects created by different libraries), in the same way that an inline class with a single field imposes a discipline on the usage of a single object.

torellifr avatar Dec 30 '22 13:12 torellifr

An inline class with multiple fields cannot be assignable to Object, which requires (the reference to) the value to be stored in a single memory location. That means you cannot have a List<MultiValueInlineClass>.

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?). By requiring the class author to choose a record shape, it becomes more configurable.

lrhn avatar Jan 02 '23 11:01 lrhn

@purplenoodlesoop wrote:

Does this feature replace the previous proposal of views?

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 avatar Jan 03 '23 10:01 eernstg

@eernstg It seems that this new proposal doesn't have the possibility to show or hide members of the original type. Or am I missing something?

mateusfccp avatar Jan 04 '23 14:01 mateusfccp

this new proposal doesn't have the possibility to show or hide members of the original type

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.

eernstg avatar Jan 04 '23 14:01 eernstg

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 IdNumber type used in the example might be required to be a positive eight digit integer, or a US social security identifier might match against a String-based regex.

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 bool get isValid that could be overridden by an inline class.

timsneath avatar Jan 18 '23 12:01 timsneath

The current version definitely do not provide any such guarantee. That's what allows us to erase the type at runtime. Because we do that, there is no way to distinguish a List<int> from a List<IdNumber> at runtime, and if we allow a cast of as List<IdNumber> at all, then it has mean as List<int> at runtime where the IdNumber type has been erased.

An alternative where we retain the inline-class type at runtime, so the type List<IdNumber> is different from, and incompatible with, a List<int>, and where we simply do not allow object as IdNumber (or force it to go through a check/constructor invocation defined on IdNumber) would be a way to keep the restriction on which values are accepted at runtime.

I don't think it's impossible. I think making is T go through an implicit operator is constructor-like validator is more viable than disallowing as T entirely when T is a reified inline class type, because that would be hard on generics. (Unless inline class types are moved outside of the Object hierarchy, and we only allow is/as checks on Object?s, but that'll just make lots of generic classes not work with them, and I don't even know what covariant parameters would do. I guess the inline class types would also have to be invariant.)

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

lrhn avatar Jan 18 '23 12:01 lrhn

@timsneath wrote:

enforcing that the type is a subset of all available values

We could choose to use a different run-time representation of an inline type (so the representation of IdNumber would be different from the representation of int, but the former probably has a pointer to the latter).

We could then make 2 as IdNumber throw (or it could be a compile-time error), and we could make 2 as X throw when X is a type variable whose value is IdNumber. Similarly, List<IdNumber> could be made incomparable to List<int>, and a cast from one to the other (involving statically known types or type variables in any combination) would throw.

I proposed a long time ago that we should create a connection between casts and execution of user-written code (so e as IdNumber and e as X where X has the value IdNumber would cause an isValid getter to be executed), but this is highly controversial and didn't get much support from the rest of the language team. One reason for this is that it is incompatible with hot reload (where the whole heap may need to be type checked, and runtimes like the VM do not tolerate execution of general Dart code during this process).

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 identical(IdNumber, int) is true, and so is 2 is IdNumber as well as myIdNumber is int).

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

eernstg avatar Jan 18 '23 18:01 eernstg

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.

timsneath avatar Jan 18 '23 20:01 timsneath

@timsneath

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.

purplenoodlesoop avatar Jan 20 '23 16:01 purplenoodlesoop

@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 Object). We do this by giving it an inline type rather than its "original" type, aka its representation type (typically: a regular class type).

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., SocialSecurityNumber vs. MaxNumberOfPeopleInThisElevator, both represented as an int).

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>>'.
}

FunPair provides a customized version of the Pair type which is suitable for creating pairs of functions that we can compose (so we're making sure that we don't make mistakes by writing Y Function(X), Z Function(Y) again and again).

In the first line of main we create a new FunPair. We need to write the type of the first argument (String), but the type inference machinery ensures that we can get the other two types correct without writing any more types. If you change FunPair to Pair in that expression, the second argument gets the type List<dynamic> Function(dynamic).

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.

eernstg avatar Jan 20 '23 22:01 eernstg

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

timsneath avatar Jan 21 '23 09:01 timsneath

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 int => SocialSecurityNumber conversion. The inverse - SocialSecurityNumber => int - can be relaxed to enable compatibility with general purpose containers, and Object/Object?. There's not much to validate when going from a subset to a superset anyway, since by definition the superset allows all elements of the subset.

One issue we'd want to avoid is reintroduce the wrapper object problem for containers, since to get a type-safe conversion from List<int> one would need to wrap it into a List<SocialSecurityNumber>, e.g. by calling map<SocialSecurityNumber>(...).toList().

This is where having as invoke the constructor of the inline class would be useful. However, we only need a limited one. Namely, as would only invoke the constructor of the outer type. It would not include any magic for converting generic types. Since List<int> as List<SocialSecurityNumber> is disallowed, one could create an inline class that validates the contents of List<int> and produces a soundly checked collection of SocialSecurityNumber. For example:

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 InlineList is a wrapperless view on List<F>. It denotes that type T is an inline class type that wraps F. This is so that expression _it[index] as T inform the compiler that T's constructor must be called and that T is an inline type that accepts F as its underlying representation. Note that the language does not need to specify, and the compiler does not need to implement, how to iterate over and type check the contents of the underlying representation (which would be the case if we demanded support for List<int> as List<SocialSecurityNumber> expression to be supported and be sound). That would be difficult to do since there could be any number of collection types, including those not API-compatible with those from the standard libraries.

Now we can safely view a List<int> as a collection of SocialSecurityNumber:

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:

  • int => SocialSecurityNumber conversion does not bypass validation.
  • There are no wrapper objects, with one caveat:
    • If SocialSecurityNumberList must be passed to a utility that takes List, it either needs to be cast back to List<int> or wrapped using InlineList.cast() method.

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:

list.where((i) => i.isOdd)

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:

list.where((i) => !i.isOdd)

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.

yjbanov avatar Feb 19 '23 19:02 yjbanov

The as you're talking about here would be like a "default constructor" or default coercion operator, from representation type to inline class type, carried by the inline class somehow.

It cannot work with the current design. When we completely erase the inline class at runtime, the _it[index] as T cannot see that T is an inline class type, because it isn't. The type of T at runtime is the representation type.

Even if we know that the T was originally an inline class, from the inline T on F bound, we don't know which one, not unless we let the T carry extra information at runtime, which is precisely what we don't do. All that information is erased. It ceased to be. It's an ex-information.

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. Same reason extension methods of a subclass doesn't work with type parameters, all you know statically is the bound of the type parameter, not the actual value, and anything statically resolved will see only that.

We could make all inline classes automatically subtype a platform type InlineClass<T> where T is their representation type. Just like we make all enums implement Enum, functions implement Function and records implement Record.

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 inline T on F.) Not sure how useful that is going to be in practice with the current inline class design, but it can allow us to add operations that work on all inline class types.

We can make InlineClass<T> assignable to T by "implicit side-cast" if we want to, without making the actual inline types so. Not particularly necessary, since you can always do an explicit as T instead. Probably not worth it.

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 as, and going through generics.

The only security you get against using an incompatible int as a social security number is that there is no subtype relation between the two, and no assignability, so you can't just assign a List<int> to List<SocialSecurityNumber>. You need to do a cast. Which makes the SocialSecurityNumber abstraction safe, but not sound. (Normal casts are sound, not safe. They throw if the conversion isn't valid. Casts from representation type to inline class types always succeeds, but doesn't necessarily preserve all notions of soundness that the inline class might want to preserve, because it cannot do checks at runtime.)

If your program contains no as casts, or dynamic values, it's probably type sound.

lrhn avatar Feb 20 '23 09:02 lrhn

@lrhn

Thanks for the detailed explanation! Yeah, this would need extra RTTI to work.

If your program contains no as casts, or dynamic values, it's probably type sound.

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 👍

yjbanov avatar Feb 22 '23 18:02 yjbanov

Is this feature will be shipped in Dart 3.0? It seems perfect to create value objects by the inline class.

Peng-Qian avatar Feb 24 '23 02:02 Peng-Qian

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.

mraleph avatar Feb 24 '23 08:02 mraleph

@yjbanov wrote:

it's worth exploring how much the soundness can be preserved with this feature.

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 List<SocialSecurityNumber> from a List<int> at run time (but we still don't have a wrapper object for an individual instance).

The main element of this feature is that an inline class can be protected, which means that it must have a bool get isValid getter which will be invoked implicitly at the beginning of every generative constructor body, and there's a dynamic error if the given representation object isn't valid.

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 i as T for some T, but also the action taken when executing ys.add(e as dynamic) when ys is a List<I> where I is a protected inline type, and also the action taken during covariance related type checks, etc.

The fact that the invocation of isValid occurs whenever there is a dynamic check for a protected inline type is quite promising, in the sense that it would probably suffice to ensure that no object o will ever have static type I where I is a protected inline type, unless o.isValid has returned true at some point.

Of course, if o is mutable and isValid depends on mutable state then o.isValid can be false in the current state. This is an issue that developers need to reason about "manually".

This would work also in the case where the source code has a type variable X whose value is a protected inline type: A run-time check against a given type which is a type variable is already performed using a thunk (a low-level function), at least on the VM. This thunk is specific to the given value of the type variable. This means that there is no additional cost for checking against any type which is not a protected inline type (they would just have a thunk which does exactly the same thing as today), and the thunk for a protected inline type can run isValid on the given object.

We could express the InlineList as follows:

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 https://github.com/dart-lang/language/issues/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.

eernstg avatar Feb 24 '23 18:02 eernstg

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 isValid returns false while one is trying to stuff a value in a list. It's like a RangeError and indicates a bug in the program. In practice, having isValid contain assertions only, and when compiled in release mode, evaporate entirely (e.g. so that a huge JSON tree can be wrapped in a typed data structure in O(1) time in release mode, even if it's validated in O(N) time during development).

But yeah, if this makes the language too complex, then this is fine as specified.

yjbanov avatar Mar 01 '23 02:03 yjbanov

Silly idea: We could have a special variant of inline classes: assert(protected) inline class I .... It would be required to declare a bool get isValid member, and it would work exactly as a protected inline class as described above when assertions are enabled. In particular, it would implicitly run isValid to check the validity of every dynamic type check for being an I, and it would throw if the given object isn't valid and the type is required (as), or it would return false if the type isn't required (is).

When assertions are not enabled it would be a plain inline class. isValid would no longer be invoked implicitly, and the run-time representation of the inline type I would now be identical to the run-time representation of the representation type.

This means that myObject as I would be a no-op (and recognized as such by the compiler) when the static type of myObject is the representation type of I. For higher-order "conversions" we'd use a few helper functions:

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 X == I (considering the intended invocation where X would be int and I would be SocialSecurityNumber or something like that), and hence we would shortcut the conversion. During a development execution we would have X != I, and the validation would occur on each element of the list.

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

eernstg avatar Mar 01 '23 17:03 eernstg

is currently possible to test this?

jodinathan avatar Mar 07 '23 11:03 jodinathan

The inline class feature can be enabled (using an experimental and not entirely complete implementation) by giving the option --enable-experiment=inline-class to a recent dev-channel version of Dart.

eernstg avatar Mar 07 '23 11:03 eernstg

@eernstg can this be used with js interop?

jodinathan avatar Mar 10 '23 18:03 jodinathan

That's a major use case for this mechanism, but I don't know the exact details about how it's being done, or the timeline.

eernstg avatar Mar 10 '23 22:03 eernstg

I wonder if we could make a js object implement the Iterable class so it can be iterated through for-in loop.

@srujzs do you have info on inline classes and js interop?

jodinathan avatar Mar 11 '23 13:03 jodinathan

We're not quite ready for release yet since we want to add a few more components to the JS interop story, like JS types. Currently, however, you can try out inline classes with JS interop using dart:js_interop's JSObject as the representation type. Inline classes and inline class interop is still a WIP, but you can check out tests/lib/js/static_interop_test/inline_class for examples on how to use these and see what's currently supported.

make a js object implement the Iterable class so it can be iterated through for-in loop

Inline classes don't support implementing non-inline classes afaik, so this isn't quite possible. That being said, we could consider making the representation type for JS types like JSArray implement Iterable. This should be doable with the JS backends, but TBD on dart2wasm.

srujzs avatar Mar 13 '23 15:03 srujzs

Inline classes don't support implementing non-inline classes afaik

True. Various proposals had mechanisms of that nature, but they were taken out because they didn't get sufficient support from the language team. The mechanism which is perhaps most likely to get reintroduced would allow an inline class to have one or more non-inline types in its implements clause, on the condition that they are supertypes of the representation type. This basically means that an inline class can be assignable to the chosen supertypes of the representation type, and it would be connected with support for calling members of the representation type:

class A ( void a() {}}
class B extends A { void a() {} void b() {}}

inline class I implements A {
  final B it;
  I(this.it);
}

void main() {
  var i = I(B());
  i.a(); // OK, will call the implementation `B.a()`.
  A a = i; // OK, `I` is a subtype of `A`.
  a.a(); // OK, same thing as `i.a()`.
  i.b(); // Compile-time error: No such member.
}

But this wouldn't address the for-statement case as far as I can see.

making the representation type for JS types like JSArray implement Iterable

I think that would be great, if possible!

eernstg avatar Mar 13 '23 16:03 eernstg