Closed munificent closed 2 years 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)
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
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?
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 view
s.
The former is an opaque object which just does not promise to retain identity.
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.
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:
@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.
@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 likeList<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".
Returning to the subject of the issue, I think we currently have (at least) the following proposals on the table:
view class
view
in UI codeextension class
inline class
facade class
Other things we should list for consideration?
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
.
@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).
In C++
int32_t
andstruct foo { int32_t x; }
are kinda the same thing. So if you want to have an abstraction onint32_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.
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.
Have we considered erasable class
? As @lrhn said above, we erase view types at runtime, collapsing them to their representation type.
How about inline class
or static inline class
?
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.
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.
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
.
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
).
@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.
That's exactly the reason why I still prefer
view
orfacade
.For comparison, consider why it's called
class
rather thanvtableRecord
:
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.
Closing. It's now 'inline class'.
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:
view
property on AnimationController (This and otherview
properties on Flutter classes are particularly bad because users often don't understand contextual keywords and get confused when a seeming keyword is also used as a user identifier.)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:
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":