language icon indicating copy to clipboard operation
language copied to clipboard

Should structs and extension structs be tied together?

Open leafpetersen opened this issue 1 year ago • 9 comments

The extension struct proposal here ties together structs and extension structs in two ways: it uses overlapping syntax (the struct keyword), and it has a goal that the constructs may be thought of as not adding new functionality, but rather simply subtracting functionality from a more general construct in order to enable new affordances. So structs may be thought of for the most part as restrictions on classes, and extension structs as restrictions on structs. This is not entirely true, since at least as proposed, primary constructors are new syntax, and there are semantic deviations around identity (in the case of structs) and runtime representation in general (in the case of extension structs), but to a first approximation it is true. A disadvantage of this is that it requires finessing around competing priorities: some restrictions we may wish to impose on structs aren't really necessary for extension structs, and vice versa. An alternative approach is to split out the constructs into two or three separate features, breaking the connection, and giving greater flexibility to the design space. In the most general form, we'd split this into three separate features:

  • Primary constructors (for classes, data classes, and views)
  • data classes (aka structs in this proposal) which can (or must?) use primary constructors.
  • view classes (aka extension structs in this proposal) which can (or must?) use primary constructors.

We could also choose to restrict primary constructors to just data classes and views, to avoid complexities around supporting primary constructors for general classes.

leafpetersen avatar Jul 29 '22 21:07 leafpetersen

cc @mit-mit @lrhn @eernstg @chloestefantsova @johnniwinther @munificent @stereotype441 @natebosch @jakemac53 @rakudrama @srujzs @sigmundch @rileyporter @mraleph

leafpetersen avatar Jul 30 '22 00:07 leafpetersen

Initial opinion: I prefer the view approach to static views, to the extension struct approach.

lrhn avatar Jul 31 '22 13:07 lrhn

I tend to think that it is unimportant to send a signal to every developer every time they use a zero-cost wrapper, indicating that it's to some extent the same thing as a value class.

In other words, I don't think it's very important to share a keyword, even in the case where we do want to share some semantics.

That said, we have the separate question about boxing and identity: If we support boxing of an expression whose static type is an extension struct S, then presumably the boxed object would be an instance of a declaration which is identical to the declaration of S, except that it does not include the word extension.

This ensures that the boxed object has the same interface members as the statically resolved members of S, and it ensures that two distinct boxings of the same object with the same underlying extension struct type will compare equal using ==, because this is a property which is guaranteed for struct declarations.

So that is one area where I think it would be useful to have a connection between the semantics of structs and of extension structs.

eernstg avatar Aug 01 '22 14:08 eernstg

I feel like bundling these two features together does not actually bring us any benefit. I can see some parallels between them - but struct being reified and extension struct being non-reified makes these two fairly distinct.

If we bundle them together then I would expect that one is subset of another or there is a clear shared core, but beyond some syntactic similarities I don't see that here.

mraleph avatar Aug 02 '22 12:08 mraleph

If we bundle them together then I would expect that one is subset of another or there is a clear shared core, but beyond some syntactic similarities I don't see that here.

I'm somewhat coming around to this, but only based on the ideas of:

  • Generalizing primary constructors to classes
  • making data classes and view classes be variants of classes.

That is, I'm not very comfortable with a world in which there are three unrelated declaration forms in play here, e.g.:

// struct/data class, has a primary constructor
struct Foo(int x, int y);
// class, unrelated to struct, no primary constructor
class Bar {...};
// view, unrelated to either
view Baz show OtherView.* hide beep {...}

That's a lot of surface area in the language and a lot of cognitive load/complexity for users.

leafpetersen avatar Aug 03 '22 20:08 leafpetersen

That is, I'm not very comfortable with a world in which there are three unrelated declaration forms in play here, e.g.:

I agree with this sentiment. I was rotating these two features in my head trying to figure out how they fit together and how things could be decomposed, but so far I was unable to find something that really makes sense.

One thing that I thought about is that if extension types on typedef types worked slightly different from how they worked currently, they maybe we would not need the whole concept of extension struct.

What if we tweaked extension resolution to permit specifying extensions on typedef types (meaning that typedef types are not eagerly substituted, but can participate in things like extension method resolution).

In this case:

typedef WrappedInt = int;
extension WrappedIntMethods on WrappedInt {
  
}

gives you almost the extension struct?

mraleph avatar Aug 04 '22 09:08 mraleph

What if we treat extension struct not as a variant of a struct, but as a variant of an extension (an extension of extension, really).

That means using extension notation: on for the type:

extension struct Foo<T> on List<T> {
  ... members ...
}

The difference between this and just extension is that it introduces a (static-only) type. It's erased as runtime, becoming just the on type.

There is no need to allow implements. Assignability and subtyping are handled separately, like in the "views" proposal, but being a subtype does not mean having the same signature. It means being usable at the supertype, not as the supertype. The value can be used where an instance of the on type is expected, because it is one, but the extension struct signature is separate from the super-interface and lost on up-cast.

Basically, "views" with new syntax.

lrhn avatar Aug 08 '22 10:08 lrhn

What if we treat extension struct not as a variant of a struct, but as a variant of an extension (an extension of extension, really).

Yes, this is a direction we've looked at. My primary objection is that I very firmly believe that the typing and scope resolution that is currently used in extension methods is wrong for views. Specifically:

  • this should have the introduced type, not the "on" type
  • this.foo should always resolve to the foo defined in the extension, without depending on the non-standard extension method resolution.

There are some examples of the differences in resolution in the two approaches here.

Given this, it seems misleading to me to build off of extension methods, since the typing is substantially different. There is no reason that I can see that view methods should be typed and resolved differently from class methods, which pushes me to unify them with classes, rather than with extension methods.

but being a subtype does not mean having the same signature. It means being usable at the supertype, not as the supertype.

I don't know what you mean here.

The value can be used where an instance of the on type is expected, because it is one,

I'm fairly opposed to this design choice. There some discussion of the choices available here.

leafpetersen avatar Aug 09 '22 20:08 leafpetersen

but being a subtype does not mean having the same signature. It means being usable at the supertype, not as the supertype.

I don't know what you mean here.

Subtyping can mean a lot of things in object oriented languages, and most of those things are often conflated.

In Dart, subtyping has so far meant substitutability, covariant non-coercing assignability, and allowing all valid supertype invocations on the subtype.

  • The instance of the subtype can validly flow into a position where an instance of the supertype is expected (substitutability/subsumption).
  • An expression statically typed at the subtype can be assigned to a context expecting the supertype (assignability).
  • Any invocation that is valid on an expression statically typed at the supertype is also valid on an expression statically typed at the subtype (allows supertype invocations). And usually with dynamic dispatch.

None of those are necessary for a "subtyping" relation. The first one is pretty fundamental for OO languages, but you could imagine a language where up-casts were coercive, say, if a three-tuple assigned to a two-tuple type would discard the third value. They could claim that a three-tuple is a subtype of a two-tuple. Assignability is so very convenient that it's hard to imagine not having it, but nothing prevents a language from requiring explicit upcasts everywhere. And we already broke the third one ourselves with covariant.

When I say that "being usable at the supertype, not as the supertype", I'm referring to breaking the third point even more.

We can allow an extension struct/view type to be a subtype of its supertype, but still have a completely different set of member signatures when used at the extension struct type, than what the supertype interface has. You'd have to cast the object to the supertype to access the supertype methods. (We can obviously provide an easy way to make forwarders from the extension struct type to the super type methods, but that's for convenience, not necessity.)

Basically, it's like viewing the object at the extension struct type only allows the "extension methods" to be called.

I say that

The value can be used where an instance of the on type is expected, because it is one,

because I expect the extension struct to be a zero-cost wrapper. The underlying object is of the on-type. If you cast it to the on type using as ThatType, it will succeed an be that type.

Member resolution at the extension struct type is necessarily static (because a zero-cost wrapper cannot represent the extension struct currently in use, that'd be a cost, so there can be no dynamic dispatch other than what's provided by the underlying object).

That does not mean we have to have automatic assignability in either direction, we get to decide that. If we don't have assignability, we may want to consider whether we have subtyping at all (because then the subtyping really doesn't buy us much). This was all under the assumption that the was a subtype relation between the supertype and the extension struct type. (And looking back, I have no idea where I got that from, since I was the first one to mention subtyping at all. It was probably primed by discussions about the implements clause.)

So, that's what I meant.

lrhn avatar Aug 09 '22 22:08 lrhn