dart-lang / language

Design of the Dart language
Other
2.62k stars 200 forks source link

Can we come up with a better name than "view"? #1974

Closed munificent closed 1 year ago

munificent commented 2 years ago

I'm generally excited about the views proposal. Zero-cost opaque wrappers sound useful. What I'm not excited about is the name. :)

Dart is primarily a client-based application language. In other words, a language for implementing UI frameworks and building GUIs. "View" is already a very frequently used word in that domain:

You get the idea.

I also agree that "extension type" isn't a good name given that we already have extends and "extensions".

A couple of (not great) suggestions:

"Constricted type"

This is probably too cute:

class Meters constricts int {
  ...
}

An extends clause gives you all the methods of the superclass. So, it's antonym "constricts" means don't take all the members of the superclass. (contracts is probably a better antonym but that's already a loaded term.)

"New type"

We could use the existing typedef keyword and call it something like a "new type":

typedef Meters of int {
  ...
}
mraleph commented 1 year ago

Maybe the reason why we are struggling to come up with a good name for views is because the feature itself is just a facet of another feature? It's like we are trying to name an elephant, but we are just touching the trunk, so we think it's a snake.

As I mentioned before maybe we don't actually need views, we just need non-reified value types - and a view is essentially just a one-field non-reified value type.

static record Foo {
  final int pointer;
}

static record Bar {
  final int ponter;
  final Memory memory;
}

// Foo(pointer: 10) runtime type is int 
// Bar(pointer: 10, memory: mem) runtime type is (int, Memory)
lrhn commented 1 year ago

It seems to me that non-reified structs are really just views on records. In that way, views and records by themselves are more fundamental concepts. We're building the elephant out of smaller parts :grin:

But let's play with this, because it is interesting.

Define struct as introducing a value-type (non-identity-preserving), with a representation type which can be a record, but doesn't have to be.

The define a struct as:

struct Point(int x, int y) {
  // members
}

Since the struct has a primary constructor which takes more than one argument, its representation is a record of those parameters (positional or named as per the parameter). If there is only one positional argument, the representation type becomes the type of that argument, instead of the singleton record. If there is zero arguments, the representation type becomes the empty record type.

Each publicly primary constructor parameter introduces a getter with the same name which extracts the corresponding value from the representation record.

A struct cannot declare fields, it's entirely defined in terms of its representation object, which is an immutable record.

Then Point is castable to (int, int), and (int, int) is castable to Point, but neither are subtypes of each other.

Further, you can define substype structs, which must share the same representation (or a subtype of it).

struct Foo(num x, num y, {int z, int w}) { 
  num get sum => x + y + z + w;
}

struct Bar(int x, int y, {int z, int w}) is Foo { }

The is-types allow you to "inherit" the non-virtual struct members.

The primary constructor is required. It defines the underlying representation. You can have other constructors, but no other non-redirecting generative ones.

We can do struct MapEntry<K, V>(K key, V value) /* is (K, V) */ {}.

With that, a view is really just a struct ViewName(RepresentationType rep) { ... }, but the struct just allows more easily wrapping up multiple arguments/fields into a record for you.

If you don't want to create a struct type, you can still use (1, 2, color: Color.red) directly, but it'll be easy to to introduce a struct and create it as ColorPoint(1, 2, color: Color.red).

I ... actually like this. So I wrote it up: https://gist.github.com/lrhn/14cb77f2c822d918484e129bd175a375

eernstg commented 1 year ago

Define struct as introducing a value-type (non-identity-preserving), with a representation type which can be a record, but doesn't have to be.

Interesting!

A non-identity-preserving value type has so far been understood to be an actual wrapper object that may be eliminated by the compilation process (and the elimination of the wrapper object is implicit in the surface language, so we can't know, but it would definitely involve a wrapper object when a reference of type Object? to the value exists).

But it sounds like there is no separate wrapper object here, so we're talking about something which is more like an extension struct in @leafpetersen's proposal (or indeed more like a view class), with the twist that we're implicitly introducing a record in the case where more than one field is declared.

One nice property of this design is that it enables us to access several instance variables of the struct/view/thing using their name directly, whereas the design where the record type is explicit (like the view proposal) would need to name the record (say, r) and then access each getter from there (e.g., r.$0).

A similar effect could of course be achieved "manually" by declaring a getter (T get $0 => r.$0;).

One thing to consider is whether structs with loosely attached (that is, statically resolved) members based on a record representation type are robust enough to be worthwhile. Let's compare them with @leafpetersen's proposal (let's call those types and entities 'value types/objects', and these ones 'record-value types/objects'):

I tend to think that whenever a record-value object can be inlined (e.g., two fields are stored in two registers, and the record itself has no representation), the corresponding value object can also be inlined; and when they are boxed, the value object has a robust semantics whereas the record-value object still relies completely on static resolution.

So wouldn't we actually prefer to have value objects rather than record-value objects, simply because the benefits provided by having a boxed representation are much more complete with the former than with the latter?

lrhn commented 1 year ago

The difference between "value objects" and "record-value objects" is that the latter can be cast to and from its (single) representation object, which is why it's applicable as a generalization of views. The former is an opaque object which just does not promise to retain identity.

munificent commented 1 year ago

So wouldn't we actually prefer to have value objects rather than record-value objects, simply because the benefits provided by having a boxed representation are much more complete with the former than with the latter?

My understanding is that a key requirement motivating this feature is that JS interop web folks need to be able to receive a List<JSObject> and be able to cast it directly to something like List<InputElement> in constant time and without any overhead. That implies to me that they really can't be reified while meeting that requirement.

leafpetersen commented 1 year ago

As I mentioned before maybe we don't actually need views, we just need non-reified value types - and a view is essentially just a one-field non-reified value type.

@mraleph

I don't know what any of the words above mean (because you haven't defined them). The phrase "one-field non-reified value type" is precisely how I would describe the existing proposal, but you seem to mean something different. For this to be useful then, you need to tell me what you are proposing, and how it is different from what we have on the table. To get started, I would suggest that there are a set of key questions/terms you need to answer/define if you want to have a meaningful discussion about counter-proposals here. Specifically:

leafpetersen commented 1 year ago

@lrhn

It seems to me that non-reified structs are really just views on records. In that way, views and records by themselves are more fundamental concepts. We're building the elephant out of smaller parts 😁

As best I can see, what you're talking about here is just having syntactic sugar where view class Foo(int a, int b) means exactly the same thing that you would currently write as: view class Foo((int, int) pair). Are there any other meaningful differences? I considered this option, and it has some appeal, but it gets kind of ugly around this bit:

If there is only one positional argument, the representation type becomes the type of that argument, instead of the singleton record. If there is zero arguments, the representation type becomes the empty record type.

This introduces some discontinuities in the feature that I personally found unpleasant. For example, consider:

view class A({int x}) {}

view class B({int x, int y}) {}

Presumably this has the behavior that: A(x : 3).x works, B(x : 3, u : 3).x works, (B(x : 3, u : 3) as dynamic).x works, but (A(x : 3) as dynamic).x doesn't work.

It's ok, but feels a bit weird to me in general.

leafpetersen commented 1 year ago

@munificent

My understanding is that a key requirement motivating this feature is that JS interop web folks need to be able to receive a List<JSObject> and be able to cast it directly to something like List<InputElement> in constant time and without any overhead. That implies to me that they really can't be reified while meeting that requirement.

It's not clear to me whether this is a hard requirement or not. It has been put out as a possible "nice to have".

leafpetersen commented 1 year ago

Returning to the subject of the issue, I think we currently have (at least) the following proposals on the table:

Other things we should list for consideration?

lrhn commented 1 year ago

Correct, my non-revolutionary idea is just to generalize views to multiple fields, auto-collected in a record. And special casing the one-element case to not use a record, which exposes how shallow a sugar it is :) Without the auto-record creation, if the struct just had multiple fields by itself, it's basically the struct proposal.

What makes the struct proposal not work as a generalization of views, is that a struct Foo(int x){ ... } is not castable to int. The Foo type doesn't have a representation type, it's not zero-overhead. Instead it's a new, reified, type that just may not preserve identity. That's necessary, because that struct Bar(int x, int y) {} cannot have a single representation type.

If we choose to say that Bar is castable to a "representation type" of (int x, int y), then we get views.

I'm trying to look for something which unifies those two approaches, so we'll only need one feature. This is one possible approach to doing that. Admittedly not perfect.

Anyway, one idea in there that might be useful, is using is as the super-view keyword. It's completely neutral about the relationship with the "super views". It just is that thing.

So, I'll suggest that as a view syntax proposal. (Just did.) For this issue, my vote is still on view class, or just view.

mraleph commented 1 year ago

@leafpetersen

As I mentioned before maybe we don't actually need views, we just need non-reified value types - and a view is essentially just a one-field non-reified value type.

@mraleph

I don't know what any of the words above mean (because you haven't defined them).

If we are going for a rigour - @lrhn has that covered in his proposal. So instead of trying to make it more rigorous let me try to explain where I am coming from.

In C++ int32_t and struct foo { int32_t x; } are kinda the same thing. So if you want to have an abstraction on int32_t you can just wrap it in struct with some methods and get equivalent performance characteristics. There is nothing special to single field structures - they just happen to have the same representation as their single member. (I am ignoring a bit of possible ABI wonkiness for the sake of simplicity).

So I am trying to look at the view feature in the same way. What if instead of making a special feature, we make a slightly less special feature that just happens to have necessary properties when you just use a single field? Then we don't have to search for a special name.

(It could have been especially clean if we could say that (T,) is just T. But @lrhn says it's impossible and too many things break).

leafpetersen commented 1 year ago

In C++ int32_t and struct foo { int32_t x; } are kinda the same thing. So if you want to have an abstraction on int32_t you can just wrap it in struct with some methods and get equivalent performance characteristics. There is nothing special to single field structures - they just happen to have the same representation as their single member. (I am ignoring a bit of possible ABI wonkiness for the sake of simplicity).

Yes! And the natural extension of this is... not what @lrhn proposes. Because in the proposal from @lrhn , moving from one field to many moves from struct foo {int32_t x;} to struct foo {int32_t x, int32_t y}*. That is, when you add a second field, you suddenly get a boxed object, which does not have equivalent performance characteristics, and does not have the same representation as the underlying values. This is very surprising to me, and I'm surprised that you feel this is what you want (I also don't understand how you feel this makes the feature simpler).

To me then, the natural future extension that I would like to consider making with views is to allow multiple fields, as you say, with the semantics that you describe: that is, you end up with something which has the same underlying representation as a sequence of objects. This implies that a view with multiple fields is not a subtype of Object, and cannot be used polymorphically. That's fine! It's useful in lots of other places - exactly as unboxed structs are useful in C/C++. So now there's no non-uniformity between single field views and multiple field views, except that, a single field view is a subtype of Object iff the thing which it is a view on is a subtype of Object.

So I am trying to look at the view feature in the same way. What if instead of making a special feature, we make a slightly less special feature that just happens to have necessary properties when you just use a single field? Then we don't have to search for a special name.

This is where you lose me. What you are proposing is not simpler than views, and doesn't avoid any complexity of views (as far as I can tell - again, maybe there's something that you're not telling me, but since you're not telling me, I don't know what it is). It is, in fact, a natural extension of the existing view proposal.

mraleph commented 1 year ago

I would like to avoid wasting too much of language team's bandwidth and avoid further derailing of the naming discussion, so I acknowledge that I don't have strong argument against views in the features current form.

One interesting naming alternative is to pick from Rust which uses transparent: https://doc.rust-lang.org/1.26.2/unstable-book/language-features/repr-transparent.html

transparent class Foo {
  // ...
}

I think other possible options could be "naked" - though it is usually used in the context of functions.

More attempts at explaining my way of thinking about views as just being single-field structs > Yes! And the natural extension of this is... **not** what @lrhn proposes. Because in the proposal from @lrhn , moving from one field to many moves from `struct foo {int32_t x;}` to `struct foo {int32_t x, int32_t y}*`. That is, when you add a second field, you suddenly get a boxed object, which **does not have equivalent performance characteristics**, and **does not have the same representation as the underlying values**. This is very surprising to me, and I'm surprised that you feel this is what you want (I also don't understand how you feel this makes the feature simpler). I agree that this jump from 1 to 2 fields is far from entirely smooth. I'd say that design here stems from the desire to fit the feature into the existing language and in the existing language records are the closest natural match. Records are designed with builtin hope (not guarantee!) that they will get unboxed - at least when they are used outside of generic/`Object` contexts and the underlying platform has capabilities to unbox them. This means we are hoping that: ```dart (int, int) foo((int, int) val) { return (val.$0 + 1, val.$0 + 1); } ``` will not have any wrapper objects allocated inside the function. Here we hit a wall with JS target though - I don't think we can achieve wrapperless compilation of this function... Though I don't think there is any _wrapper-less structure type_ design which can be efficiently supported on the JS target. By the way native ABIs are also not entirely smooth: ```cpp template struct S { int64_t x[N]; }; // both input and result are on the registers. // S<1> is a no-cost abstraction of int64_t. S<1> nop(S<1> s) { return s; } // nop(S<1>): # @nop(S<1>) // mov rax, rdi // ret // both input and result are in memory (on the stack) S<4> nop(S<4> s) { return s; } // nop(S<4>): # @nop(S<4>) // mov rax, rdi // movaps xmm0, xmmword ptr [rsp + 8] // movaps xmm1, xmmword ptr [rsp + 24] // movups xmmword ptr [rdi + 16], xmm1 // movups xmmword ptr [rdi], xmm0 // ret ``` > To me then, the natural future extension that I would like to consider making with views is to allow multiple fields, as you say, with the semantics that you describe: that is, you end up with something which has the same underlying representation as a sequence of objects. I agree that this is a cleaner semantics though it sends us down unpaved road of introducing new types which are not subtypes of `Object?`. As noted above, I think, multi-field-views are not going to have universal performance guarantees due to limitations of the Web platform and probably will have performance (and implementation) similar to records. Which then begs the question: why not records? I understand that records don't have any platform specific performance guarantees - and multi-field-views can, for example, come with some builtin expectations for different platforms. But then again - we could just set the very same expectations for records. > This is where you lose me. What you are proposing is not simpler than views, and doesn't avoid any complexity of views. That is true. I am approaching this from slightly different angle here: not from the perspective of the language semantics or how features decompose - I am just trying to say that we could consider taking a bigger feature that has necessary properties as its part and then we don't have to give any special name to that part. It's just `struct`, it comes with static dispatch for methods and when it is a single field it has a direct transparent representation instead of wrapping fields into a record. No need to name this subfeature a _view_. No need to come up with special syntax.
mit-mit commented 1 year ago

Have we considered erasable class ? As @lrhn said above, we erase view types at runtime, collapsing them to their representation type.

TimWhiting commented 1 year ago

How about inline class or static inline class?

leafpetersen commented 1 year ago

I would like to avoid wasting too much of language team's bandwidth and avoid further derailing of the naming discussion

@mraleph I always value your input (and in general, think it is valuable both to hear other perspectives, and to get feedback about how a feature looks from "the outside").

I agree that there is some sense in which if we think we might generalize this to multiple fields in the future (regardless of semantics), there could be some value in using the keyword struct. I'm a bit hesitant to consume that keyword for this feature however, especially as it's not clear we would generalize it, and how.

The syntax transparent does have some things to recommend it, but it is a bit suggestive to me of a lack of type abstraction: that is, I would expect for a transparent thing for the methods of the underlying representation to be present.

Have we considered erasable class ?

@mit-mit yes, we've talked about this. I don't love it. I think "erasure" is probably somewhat esoteric, I'm not sure most users would understand what we mean by this. I also don't like erasable: it implies that something can be done to this class ("it can be erased") without saying who does the erasing, or what it means to be erased.

How about inline class

@TimWhiting This is my preferred choice right now. It suggests very directly the semantics ("you inline the fields of the class directly instead of wrapping them"), and it connects up with Kotlin inline classes which, while slightly different semantically (they using autoboxing) have very similar restrictions and use cases.

My second most preferred choice (ignoring view for now) is extension class, which points fairly clearly at the dispatch semantics.

lrhn commented 1 year ago

My problem with most of the suggested names is that I'd have no clue what they mean, if I didn't know what we were discussing already.

For inline class, I can sort-of see that the entire instance is being "inlined"/allocation-sunk and the methods therefore being static dispatched. It just feels like it's explaining the implementation strategy of something, without saying what it is. Same for erasable class, feels like an implementation strategy leaking into the concept. (And same for extension class.)

I like view because it says what the thing is, not how it's done. I'd even drop the class, because it is not a class. It's a new thing, with a different behavior. I'd embrace that instead of trying to pretend it's a class.

It's only potentially confusing because there are multiple ways to be a "view", because it's such a generic word.

munificent commented 1 year ago

It just feels like it's explaining the implementation strategy of something, without saying what it is.

For a feature that exists almost entirely for performance reasons, I think that is arguably a good thing. The implementation strategy is the point. If you didn't care how it was implemented, you'd probably just write a class.

I don't think transparent or erased quite work. naked is short but probably a little too suggestive. Another idea in that vein is unwrapped, but I don't like the idea of a modifier with a negative prefix.

I'm fine with extension class. I like inline class.

mraleph commented 1 year ago

The syntax transparent does have some things to recommend it, but it is a bit suggestive to me of a lack of type abstraction: that is, I would expect for a transparent thing for the methods of the underlying representation to be present.

Yeah, it transparent work better in Rust because it is actually repr(transparent) so it is clear that we are talking about representation rather than delegation.

Did you ever discuss just going for type rather than class?

type Foo {
  // ...
}

I was thinking about keywords that could indicate that something exists only in compile time and not in runtime. type might be one of them (I am drawing similarity with typedef).

eernstg commented 1 year ago

@lrhn wrote:

[inline, erasable, extension] feels like an implementation strategy leaking into the concept

That's exactly the reason why I still prefer view or facade.

For comparison, consider why it's called class rather than vtableRecord: With a traditional OO perspective, it's modeling the members of a set of similar entities, 'objects', and the fact that they're basically enhanced records where many languages support method invocation using a vtable is just an implementation detail.

munificent commented 1 year ago

That's exactly the reason why I still prefer view or facade.

For comparison, consider why it's called class rather than vtableRecord:

Sure, but given that Dart already has classes, what reason would a user have for reaching for one of these things instead of using a class? What is the motivation for preferring this feature over a regular class declaration? As far as I know, the only benefit this feature has over a class that wraps a value is performance. If that's the case, it seems to me like the name should reflect that.

eernstg commented 1 year ago

Closing. It's now 'inline class'.