Closed munificent closed 2 years ago
I'm also of this opinion. I have some thoughts that I need to write up in more detail that cover a broader set of use cases than just static wrappers, but for the static wrapper portion I'm initially proposing the syntax:
static class Meters is int {
// No fields allowed here, but otherwise normal class members allowed
}
facade Meters on int {
}
For me view
does accurately describe the feature so I like it from that perspective. I agree its an overloaded term but it doesn't appear in the actual type names so it won't directly conflict in terms of the global scope, I think its probably OK.
I do also like facade
, although I am slightly concerned it might be more confusing for people who don't have English as their primary language.
I agree its an overloaded term but it doesn't appear in the actual type names so it won't directly conflict in terms of the global scope, I think its probably OK.
But it will directly conflict with the more familiar uses of the term in many user's minds, potentially making it harder for them to understand both code and documentation that uses the feature.
The term "facade" must be known to programmers because this is the name of a pattern.
True, but this feature doesn't seem to me to match the semantics of the facade pattern very well. This feature seems closer to the adapter pattern to me.
I like @leafpetersen's suggestion, but I'll offer the following as minor variations:
static class Meters adapts int {
// No fields allowed here, but otherwise normal class members allowed
}
or
adaptor Meters is int implements UnitOfMeasure {
// No fields allowed here, but otherwise normal class members allowed
}
"New type"
We could use the existing
typedef
keyword and call it something like a "new type":typedef Meters of int { ... }
I like calling it a "new type"--it matches my understanding of the semantics of the proposal. What if we actually used newtype
as a keyword to introduce the declaration? I.e.:
newtype Meters on int {
...
}
(Note that there's prior art - Haskell has a construct called newtype with similar semantics).
for the static wrapper portion I'm initially proposing the syntax:
static class Meters is int { // No fields allowed here, but otherwise normal class members allowed }
This isn't bad. I like using is
. I'm a little less keen on class
because it feels a little strange to use that when the base type might not be what the user typically thinks of as a "class". (For example, it could be on an enum or instantiation of a generic type.)
Maybe:
typedef Meters is int {
...
}
So basically typedef ... = ...
means "same type" and typedef ... is ...
means "new type". Thoughts?
I like calling it a "new type"--it matches my understanding of the semantics of the proposal. What if we actually used
newtype
as a keyword to introduce the declaration? I.e.:newtype Meters on int { ... }
(Note that there's prior art - Haskell has a construct called newtype with similar semantics).
@sigurdm proposed something very similar here. I'm not strongly opposed to newtype
as a keyword, but it doesn't feel very Darty to me for some vague reason.
This isn't bad. I like using
is
. I'm a little less keen onclass
because it feels a little strange to use that when the base type might not be what the user typically thinks of as a "class". (For example, it could be on an enum or instantiation of a generic type.)
I'm not 100% wedded to this syntax, but my reasoning boils down to two things.
First, I want to make the syntax and the scoping of the members be essentially identical to that of classes. That's my main objection to building this off of extensions: it gives the members odd scoping rules for no good reason. It doesn't bother me that the thing being wrapped may not be a class, the point is that the user can essentially read and write the wrapper code as if it were a class with some restrictions placed on it (e.g. no fields allowed).
Second, I want to propose having both static and non-static wrappers, and non-static wrappers are essentially just a variant of classes without identity. So I would propose to have static class Foo is T
be a static wrapper (the representation is exactly T
), and class Foo is T
be a dynamic wrapper with identity based on the identity of T
plus the wrapper type. This would let you implement (e.g.) Float32x4
as:
class Float32x4 is double, double, double, double { // members here }
with the semantics that the compiler is free to eliminate or re-materialize the wrapper as needed (since a Float32x4
objects is identical to another object iff both are Float32x4
instances (which may be known statically or not) and the 4 sub-components are pointwise identical. Wrapper classes would be sealed, though I believe we can allow them to implement interfaces.
I think this addresses a long-standing request from @mraleph and company.
I can imagine using other syntax for this (for example, you could use wrapper
and static wrapper
or wrapper class
and static wrapper class
, or newtype
with a modifier for whether it has auto-boxing or not). But there is an appealing simplicity to me to the idea of simply building this as a variant of classes.
I don't understand what "is" signifies here. The whole point of Foo is that it is not a T.
For this feature, the whole point is that Foo
is exactly represented as a T
.
static class Foo is int { // no operations at all } var foo = Foo(1); if (foo is int) foo + 1;
That program will be a static error, since int
is not a subtype of Foo
, and hence there is no promotion in the body of the if
, and hence the addition is ill-typed. The following related example however will type check, and will print "2".
static class Foo is int {
// no operations at all
}
Object foo = Foo(1);
if (foo is int) print(foo + 1);
That program will be a static error, since
int
is not a subtype ofFoo
, and hence there is no promotion in the body of theif
, and hence the addition is ill-typed.
I have to admit it does feel a little weird to use is
here where it means "declare at type that is not a subtype of the RHS" when the point of an is
expression is to ask "is the type of this a subtype of the RHS?"
It looks and reads nice, but maybe it sends the wrong signal.
Second, I want to propose having both static and non-static wrappers, and non-static wrappers are essentially just a variant of classes without identity. So I would propose to have
static class Foo is T
be a static wrapper (the representation is exactlyT
), andclass Foo is T
be a dynamic wrapper with identity based on the identity ofT
plus the wrapper type.
Ah, interesting. And it looks like you want to allow multiple fields for the non-static wrapper case?
We may also want to factor Erik's proposal of having both "open" and "closed" static wrapper types where the former are subtypes of their base type and the latter are not.
Syntax | Can reify type? | Has identity? | Is subtype of base type? |
---|---|---|---|
static class X is Y |
No | No | No |
??? | No | No | Yes |
class X is Y |
Yes | No | No |
??? | Yes | No | Yes |
class X { var Y; } |
Yes | Yes | No |
class X extends Y |
Yes | Yes | Yes |
Do I have that roughly right? If we want to support a flavor of wrappers that allows multiple fields, then maybe we should think about designing this in concert with requests for delegation and perhaps tuples/records.
Maybe the way to model a zero-cost wrapper type is by declaring a specially-marked class with a field that defines the base type? We could maybe do something like:
// Purely static type, not reified, no identity, not subtype of base type;
static class X {
Y y;
// Other members...
}
// Purely static type, not reified, no identity, yes subtype of base type;
static class X extends Y {
// Other members...
}
// Reified type, no identity, not subtype of base type;
record class X {
Y y;
// Other members...
}
// Reified type, no identity, yes subtype of base type;
record class X extends Y {
// Other members...
}
// Reified type, yes identity, not subtype of base type;
class X {
Y y;
// Other members...
}
// Reified type, yes identity, yes subtype of base type;
class X extends Y {
// Other members...
}
I have to admit it does feel a little weird to use
is
here where it means "declare at type that is not a subtype of the RHS" when the point of anis
expression is to ask "is the type of this a subtype of the RHS?"It looks and reads nice, but maybe it sends the wrong signal.
Possibly. Other words we could consider would be wraps
or hides
?
Ah, interesting. And it looks like you want to allow multiple fields for the non-static wrapper case?
And the static case actually. Such a type would not be a subtype of Object - it would be an aggregate type (like an unboxed struct in C) which could only be used mono-morphically.
We may also want to factor Erik's proposal of having both "open" and "closed" static wrapper types where the former are subtypes of their base type and the latter are not.
I'm pretty disinclined to incorporate this as subtyping. I would prefer to incorporate it as assignability, if at all (e.g. add implicit constructors to allow assigning an int to a Meter
location.
Do I have that roughly right?
Mostly. The question of reification is the point that I'm most undecided on. The instances of course are not reified, but the question is whether <Meter>[]
is reified as List<Meter>
or List<int
> (given static class Meter is int
). The difference is whether <Meter>[] as List<int>
succeeds or throws. I'm leaning towards non-reification, but I'm not sure.
If we want to support a flavor of wrappers that allows multiple fields, then maybe we should think about designing this in > concert with requests for delegation and perhaps tuples/records.
I definitely would like to support delegation. One possibility is to say that you can declare abstract members (or use external instead) and any such members delegate. Another is to list a set of members from the wrapped type to include in the interface, possibly using something like the show/hide lists that @eernstg proposed for views.
The tuples/records question is one that has worried me - there's a lot of overlap and potential interference between the two. For example, the reason I'm using class Float32x4 is double, double, double, double
instead of the more readable class Float32x4 is (double, double, double, double)
is that I expect the latter will collide with the eventual tuple syntax. I've also been considering whether you should be able to name the wrapped components (e.g. class Point2 is {int x, int y}
) which again starts to collide with tuple and record syntax.
The question of reification is the point that I'm most undecided on. The instances of course are not reified, but the question is whether
<Meter>[]
is reified asList<Meter>
orList<int
> (givenstatic class Meter is int
). The difference is whether<Meter>[] as List<int>
succeeds or throws. I'm leaning towards non-reification, but I'm not sure.
This is honestly my biggest concern with the entire feature overall. It feels really weird to me to have types that feel like concrete usable types to the user, but where the abstraction clearly leaks in strange ways when they use it as a type argument.
For example, the reason I'm using
class Float32x4 is double, double, double, double
instead of the more readableclass Float32x4 is (double, double, double, double)
is that I expect the latter will collide with the eventual tuple syntax.
Is that a problem, or a solution? It seems like one approach we could take is that zero-cost wrappers only wrap a single value, and if you want to wrap a composite, you just wrap a tuple/record with the fields you want.
Is that a problem, or a solution? It seems like one approach we could take is that zero-cost wrappers only wrap a single value, and if you want to wrap a composite, you just wrap a tuple/record with the fields you want.
That depends hugely on the precise semantics of tuples/records. If identity on tuples is recursive pointwise identity, and tuples are immutable, then I think using a tuple as the hidden type admits the correct representation for the non-static case. Otherwise, not. And since we haven't nailed down everything with tuples, I'm hesitant to rely on this. Also, for the static wrapper case, if we want to support this we probably don't want it to be tuples. That is, static class UnboxedPair is double, double {}
, if we support it, should always be a register/stack pair, not a heap allocated object. It may be that the distinction doesn't matter: if we set it up a certain way then again you can treat it as a tuple, but always choose to represent it as a register pair. It depends a bit on what meaning we want to assign to void Function(UnboxedPair)
. One interpretation is that UnboxedPair
is reified, and that this type is not coercible to any other function type. That interpretation probably admits re-using tuples (assuming again the appropriate identity semantics). Another interpretation is that UnboxedPair
is a shorthand for "two arguments passed in registers", and void Function(UnboxedPair)
is reified as void Function(double, double)
and can be coerced to that type at runtime. That interpretation does not, I think, admit the tuple/record re-use.
That is,
static class UnboxedPair is double, double {}
, if we support it, should always be a register/stack pair, not a heap allocated object.
If we support it, we have introduced tuples. That is a tuple, just without the nice literal syntax. It's unclear whether it's even a subtype of Object
(that would require auto-boxing, and then it's not always "not heap allocated"). It's a completely new kind of type with all the characteristics of a tuple (multiple values with no own identity).
I'd hold of on supporting anything like that until we do introduce tuples, and then allow you to make a "view" on a tuple type. I don't want two different kinds of tuples in the language.
I proposed using view
for the zero cost abstraction because it's about what is in the eyes of the beholder. Merriam-Webster describes the relevant meaning of the word 'view' as "a mode or manner of looking at or regarding something".
So the point is that we have an existing object (an instance which is typable as the on-type of the view), and we wish to look at that object in a particular way, and the view allows us to do just that: We get an interface which is created independently from the interface of the on-type (we can throw away or add members freely), but it is statically known that we're actually working on an instance of the on-type (and we'll have to box the underlying object if we want to forget about this, and then it's a full-fledged object, not just a view). Another word for the same concept is 'perspective', but we usually prefer short words if possible.
This is a relevant way to understand all those other usages that @munificent mentioned:
view
property on AnimationController: This is an instance variable which is basically used to upcast an AnimationController
to Animation
(which is again closely related to user interface elements, that is, to views in the same sense as several earlier bullets).Map
, providing a specific perspective on the wrapped object.All those usages of the word 'view' seem to fit the understanding that it's about supporting a specific perspective or view on some other objects, which is suitable for someone who is looking at them. (Presumably, it does not work in practice to simply show the object "as is", we always need a customized perspective or view on the object that fits the application purpose and context). Those other objects matter, however, because we want their state to continue to exist even in the situation where we encounter them using different views.
It is true that 'view' has been used in particular to describe user interface elements, presumably because of the terminology introduced by the model-view-controller design pattern, but also because user interface elements are very much about presenting existing objects in a manner which is useful for the beholder (in this case: the end user of a GUI application).
However, this doesn't conflict with the use of the word 'view' to designate a presentation of an existing, underlying object, even though the beholder is a developer who is going to work on that object using a different interface than that of the underlying on-type.
@munificent also wrote:
This and other
view
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
It is indeed a source of confusion that view
is already being used as an identifier. However, I think that will be a rather small bump to get over, especially because view
used as the first word in a class-like declaration stands out visually, and the use of flutter members named view
is supported by having view
as a completion, and seeing existing code where view
is used as an identifier. Perhaps it would make this clash ("keyword and identifier at the same time?!") more familiar and hence less confusing. ;-)
view: - Visual perception.
outlook: - A place from which something can be viewed
facade: a deceptive outward appearance. a way of behaving or appearing that gives other people a false idea of your true feelings or situation
constricted: narrowed, especially by encircling pressure.
scene: one of the subdivisions of a play.
While looking at those definitions I found "scene" that I find fitting With the expression "behind the scene", it's almost poetic :D
Although I have encountered the keyword view
a lot more as a dev, for example in some databases (postgres) So it is more intuitive.
@lrhn
If we support it, we have introduced tuples. That is a tuple, just without the nice literal syntax.
No, tuples are structural types, these are nominal. This is more analogous to a class which has a single field, which is a tuple. But yes, the overlap concerns me.
It's unclear whether it's even a subtype of
Object
It would not.
(that would require auto-boxing, and then it's not always "not heap allocated").
For that, you use the non-static variant: class BoxedPair is double, double
.
Although I understand the confusion that view
may cause because of the diverse semantic it already has, I think it's a good name, and don't think this confusion will last more than a few minutes.
I also liked typedef
as an alternative, but I am not sold into using of
and is
. They don't sound natural to me...
Instead of is
we could consider as
to use words already known in dart.
static class Foo as int {
// ...
}
This sort-of indicates that a cast is needed to go from one to the other...
I'd say is
is also already known; is
is a reserved word, but as
is only a built-in identifier. I think any word could work here grammatically, static class
is unique.
It feels backwards to me to say Foo as int
because as int
gives me something of type int
. I'd almost prefer
static class int as Foo { ... }
which read as "the static class int-as-Foo".
(But then I'd prefer on
or extends
, if we want something already used for something similar in Dart. This is very close to extensions, which is basically a wrapper on top of int
, just without a type.)
Sorry if this isn't helpful, but I was looking at on line thesaurus for alternatives to view. I like the words "way" and representation, which could be shorted to "rep". Anyway maybe "way" could be a cool Keyword for something else.
What're some thoughts on using on
? I know there's been aversion to extends
or extension
but the on
clause is valuable because it says "this class becomes its own types and is sort of 'placed on' another type". It also draws the connection to extension types which is valuable despite views not actually being extensions. I also really like typedef
, since it already has the semantics of creating a new way to refer to an existing type. So how about:
typedef Foo on int { }
Actually, extension type
is just the old name for view
. The proposal where it's named view
has been updated several times since then, so the extension type
proposal would need to be updated in various ways in order to reflect recent discussions. In any case, I came up with the name view
because extension type
basically did not provide any hints about the actual nature of the mechanism.
I personally find static class ...
to be more confusing than view
.
<name> B on A {}
feels quite intuitive in that the way it is applied to existing types feels similar to extension B on A
.
For the same reason I also prefer on
over is
, as
, ...
Maybe some more candidates for <name>
:
cover B on A {...}
or envelope B on A {...}
zerocost B spades A { ... }
let's go full cryptic
I'm surprised that nobody mentioned this yet, but apparently views are similar to the upcoming Java value objects (https://openjdk.java.net/jeps/8277163) or the Kotlin inline classes (https://kotlinlang.org/docs/inline-classes.html). In my opinion, the view
word is better than inline
and value
. My 2 cents is the treat
keyword. treat Password as String {}
I've previously been very sceptical of earlier proposals to call this an 'extension class' as here:
extension class Meters on int {
...
}
My issue with that syntax was, that I find "extension" suggests adding behavoir, so I read this as "extend int with the following methods", which isn't what happens (the APIs on int are not available).
However, we've now switched to the primary constructor syntax in the view class proposal:
view class V1(int i) {
void v1() {}
}
In this new syntax I think extension class
is less confusing than in the older syntax:
extension class V1(int i) {
void v1() {}
}
You could argue that this reads "here is a class V1, which is a collection of extension methods, and which has an underlying representation of int".
This especially makes sense if we allow "inline" extension methods on regular classes:
class C1 {
void foo() {}
extension void bar() {}
}
I'd think that then an extension class can be explained as a class where all the members are extension methods?
I'd think that then an extension class can be explained as a class where all the members are extension methods?
Right, but it could be a bit confusing that they always extend Object
, not the representation type.
By the way, here's another one:
skin class S1(int i) {...}
It's a thin, protective layer around the representation, and it's able to look quite different if needed.
I'd think that then an extension class can be explained as a class where all the members are extension methods?
Right, but it could be a bit confusing that they always extend
Object
, not the representation type.
I don't think that's particularly confusing. There's nothing in the syntax of:
extension class V1(int i) {
That suggests V1 extends int
. Instead, it looks like V1 contains an int. (Which is basically true, except that because V1 has no other identity, it disappears entirely at runtime, leaving only the int behind.)
There's nothing in the syntax of:
extension class V1(int i) {
That suggests V1 extends int
But that's exactly what I said: If the short explanation about the purpose and meaning of V1
is going to be "it extends Object", and every other extension class
are explained by literally the same phrase, then those explanations are not particularly helpful.
It seems more informative to me to say that V1
supports a different way to see an int
. I don't seem to need the word 'extension' to say that.
Another alternative name could be twin
, then Pointer
and int
are going to be twins.
But that's exactly what I said: If the short explanation about the purpose and meaning of
V1
is going to be "it extends Object", and every otherextension class
are explained by literally the same phrase, then those explanations are not particularly helpful.
I'm not sure I entirely follow you here. If your concern is that "extension" is itself confusing because it sounds like it has something to do with "extends", I agree. We talked about that way back when adding extension members to Dart, but it seemed then like it was the most established term. In practice, users don't seem to be too confused by it, at least not that I've noticed.
So I think "extension" is fairly well established to mean "static dispatch thing" as it does in C#, Kotlin, etc.
It seems more informative to me to say that
V1
supports a different way to see anint
. I don't seem to need the word 'extension' to say that.
"Different way to see" doesn't convey much in terms of actual semantics though. Whereas seeing "extension" tells you something very concrete in terms of how the defined type behaves. All of the members in it are extension members with extension member dispatch. (It's not exactly the same because of the sticky scoping, but it's close.)
view
If your concern is that "extension" is itself confusing because it sounds like it has something to do with "extends", I agree.
That is the main point: An "extension" is something that extends another thing, thus making it bigger. This works fine for extension on int {...}
, because it makes the interface of any int
bigger by adding new members. So it's a perfectly good fit for the extension methods that we have today.
However, for a view class V1(int i)
respectively extension class V1(int i)
etc., the only thing which is made bigger is the interface of Object
(no matter what the representation type is), and it's not the main purpose of this mechanism to enlarge the interface of Object
.
So yes, we can pretzel ourselves into focusing on the fact that this mechanism will add methods to Object
, but that is not what this mechanism is about. It's about giving the representation object a new interface, that is, to make it look different, even though it's still the same object.
"extension" is fairly well established to mean "static dispatch thing" as it does in C#, Kotlin, etc.
If you look here, you can see that C# extension methods are described as follows:
Extension methods in C# enable you to "add" methods to existing types without creating a new derived type, recompiling, or otherwise modifying the original type.
The focus is clearly on the fact that we're enlarging the interface of the original type. They use double quotes around the word 'add', indicating that there is something about this operation which is a little bit different from actually editing the declaration of the original type, but they downplay this difference.
So they prefer to say '"add" methods', focusing on the similarity between instance methods and extension methods, rather than taking the opportunity to emphasize that you get a new feature here: Static resolution!
That's because static resolution is not the point.
Kotlin provides the ability to extend a class or an interface with new functionality without having to inherit from the class or use design patterns such as Decorator
Extension methods, in Dart and in those other languages, are described in terms of extending existing interfaces (of classes, mixins, interface
s, etc). So they all think that an "extension" is about enlarging an interface.
The fact that this kind of method is resolved statically is a technical trade-off: It is fairly easy to implement this feature using static resolution, and it would be a quite complex exercise to support real OO dispatch (especially in the context of separate compilation).
The main purpose of a view class
is to give the representation object a new interface.
You could say that View
is well established as a UI concept, it's about widgets and not a language mechanism.
However, cf. this wikipedia page about the model-view-controller architecture, you can see that the notion of a 'view' in this context is also concerned with the way we are looking at the model objects:
a
View
represents some way of displaying information to the user, .. AView
is also coupled to a model object, but the structure of that object is left up to the application programmer
Again, they emphasize that the view is connected to a model object (or several, of course), but it is different from the model object.
This distinction is necessary because the view presents the model object to the user in a particular context and for a particular purpose, so the view needs to support the relevant perspective on the model to serve that purpose.
I think this fits quite nicely with the understanding that a view class
supports the definition of an interface (independently of the interface of the representation object), and it allows us to "view" the representation object through that interface.
I completely agree with @eernstg regarding the extension name.
I don't have any data to back this up, but my general experience says that 90% of Dart users (or at least Flutter, which is what I work with) don't really know that extensions are static dispatch. And even among the ones that know it (like me), for most of them the word "extension" won't evoke the concept of static dispatch.
Instead, the word "extension" evokes the concept of [sic] extension. As defined by the Cambridge dictionary:
the fact of reaching, stretching, or continuing; the act of adding to something in order to make it bigger or longer.
Thus, the idea of, as @eernstg said, "adding". An extension on T
is something that adds fields, getters or methods to T
.
Being so, I would find it really confusing if "views" were called "extensions".
So, static class
?
Fwiw, I still like view
:)
It looks like Flutter may be adding classes named FlutterView
and View
.
From our recent discussion about the feature, I think we want to emphasize two things to users:
This construct defines a new interface for some underlying type. It hides the type and provides its own different set of methods.
This new interface is a somewhat leaky abstraction. There is no guarantee that all instances of the type went through the type's own constructor since you can cast directly from the underlying type to the new type. Also, a List<UnderlyingType>
can be cast directly to List<MyNewType>
.
This leakiness is by design. We want the feature to behave this way because that's what gives it the performance that is motivating it.
But since the leakiness is surprising, I think it's worth emphasizing that. We don't want users to think that they are defining a fully encapsulated type with all of the safety and maintainability guarantees that entails. (If they want that, that's what a class is for.)
If we want a word that means "a new appearance for a thing, but a somewhat false or incomplete one", I think it would be very hard to do better than Nate's earlier suggestion of facade
. I think it conveys what we want it to convey, is short, and (critically to me, is unlikely to confuse users by sounding like it has something to do with user interfaces).
critically to me, is unlikely to confuse users by sounding like it has something to do with user interfaces
I've heard this concern from you quite a bit, and I have to be honest I don't know how to evaluate it. I'm not saying I don't believe that there could be something there, but since it's not really an analytic statement, it feels like we can only go by either direct empirical evidence (see people being confused by it), or by analogy to other constructs. And I don't really have much to go on from the former - I don't think I've seen anyone yet actually confused (but of course, no-one has used it).
For the latter, I have to admit I don't find good evidence that people find it confusing when a term which is used in one domain (here "view" in the MVC sense) is used in an analogous but different sense in a different domain (here "view" in the PL construct sense). For example, there is a Switch
class in Flutter, and there is a switch
statement in Dart. Is this confusing? Swift has a thing which is called a protocol
which is not a protocol in the networking sense (which is a concept/sense which was in broad use for decades before Swift was around), but I don't think I see people being confused by this.
Again, I find this objection troubling, precisely because I can find neither good grounds to accept it, nor good grounds to reject it.
For example, there is a
Switch
class in Flutter, and there is aswitch
statement in Dart. Is this confusing? Swift has a thing which is called aprotocol
which is not a protocol in the networking sense (which is a concept/sense which was in broad use for decades before Swift was around), but I don't think I see people being confused by this.
Those are both good points. (I'm a little surprised they called it Switch
and not Toggle
because, even if it's not actually confusing, it's annoying to have a type whose lowercase name is a reserved word.)
Again, I find this objection troubling, precisely because I can find neither good grounds to accept it, nor good grounds to reject it.
I think it won't be the end of the world if we stick with view
, but it definitely hasn't grown on me over time. But that's mostly just my own aesthetic preference, and I'm not sure how much to read into it either.
- This construct defines a new interface for some underlying type. It hides the type and provides its own different set of methods.
- This new interface is a somewhat leaky abstraction. There is no guarantee that all instances of the type went through the type's own constructor since you can cast directly from the underlying type to the new type. Also, a
List<UnderlyingType>
can be cast directly toList<MyNewType>
. This leakiness is by design. We want the feature to behave this way because that's what gives it the performance that is motivating it. But since the leakiness is surprising, I think it's worth emphasizing that. We don't want users to think that they are defining a fully encapsulated type with all of the safety and maintainability guarantees that entails. (If they want that, that's what a class is for.)
It's not an exact match, but typedef
most accurately captures the idea of "a new name for an existing type". Developers are already used to declaring new members for a typedef:
typedef Json = Map<String, dynamic>;
extension on Json {
String encode() => jsonEncode(this);
}
It's true that those members are technically available on any Map
as well through as
, and that corresponds to the "leakiness" you describe:
void main() {
Json data = {"hello": "world", "num": 42};
Map badData = {42: "num"};
data.encode(); // okay
badData.encode(); // compile-time error
(badData as Json).encode(); // runtime error
}
The main differences, off the top of my head, are:
Tyepdefs are implicitly converted to and from their original types:
typedef EvenNumber = int;
int divideByTwo(EvenNumber x) => x ~/2;
void main() => divideByTwo(3); // okay
Views can't hide members or declare new members that only apply to the view:
extension on EvenNumber {
int get half => this ~/ 2;
}
void main() => print(3.half); // okay
So views are less leaky than typedefs, but more flexible in those exact ways, I still think it's okay to reuse that syntax, since they fundamentally do the same thing: assign different names to the same type.
On that note, it'd probably be helpful to explicitly document the differences between views and typedefs, and when to use which one. For example, the common case of Json = Map<String, dynamic>
could be rewritten using the proposed view semantics rather than typedefs. Existing code would need to handle the conversion a little more carefully, but the idea of "here's special behavior for certain types of Maps" stays the same.
(Taking the idea to the extreme, if views are more-or-less "safer typedefs", perhaps they can be introduced in such a way to replace the existing typedefs? Unless I missed something, I don't see anything that typedefs can do today that views can't do. Obviously, the change in syntax is painful, but maybe it's worth discussing.)
typedef Json = Map<String, dynamic>;
extension on Json { String encode(); }
// vs
typedef Json(Map<String, dynamic> data) { String encode(); }
Typedefs are a good comparison point for views. I even suggested using typedef
as a keyword for "extension types" in #42, because I was assuming mutual assignability between the view type and the representation type.
Same for just making every extension Bar on Foo { ... }
introduce Bar
as a type. That would work just as well.
The problem is that the views we are currently pursuing are not type aliases, they are new types that are not (statically) a subtype or supertype of the representation type.
We erase view types at runtime, collapsing them to their representation type.
We erase type aliases at compile-time, expanding them eagerly to the type they alias. That makes a difference.
It affects assignability. A type alias is the type it aliases at compile-time, so it can be assigned in either way. A view is not assignable to or from its representation type. (We choose not to make it assignable to prevent accidental loss of abstraction through normal value flow. We don't try to prevent deliberately taking values out of, or putting values into, the view type.)
It affects which static members you can call on the view type and alias.
We allow you to call static members of the aliased class/mixin type's declaration through a type alias.
(That is admittedly an elaborate hack, which pretends that a type alias is a declaration alias in some specific cases. It only works when it aliases something which actually has a declaration, meaning it aliases a class or mixin type, and will probably include view
types too in the future.)
The view type has its own static declarations, so ViewType.staticMember
will not expand the view type to the representation type, but will access the static members of the view declaration itself.
One of the use-cases for the generalized typedef
is to rename a class, and keep an alias for it at its own name (suitably deprecated), so existing code can keep working. The type alias ensures that either name can be used for the same thing.
I quite like facade
too. It should be familiar to those who know romance based or influenced languages (facciata
/ fachada
/ façade
/ etc.)
The word "Facade" is already used for a design pattern which I don't think fits the view design precisely enough. A facade is usually a simplified interface on top of a more complicated sub-system. The view is more like an Adapter.
The facade pattern is actually one of the reasons I like the idea of using facade
for this feature. In both, you have an object with some original, likely more complex, interface and you are deliberately hiding it to replace it with another one.
In both, you have an object
In the original design pattern it's unlikely you'd have an object. In GoF I think there is an emphasis on the facade hiding complexity from multiple other classes. I could understand "facade" feeling jarring against the limitation of wrapping a single object for anyone who most commonly thinks of the GoF pattern when they read "facade".
Searching internally, I can find usage of the word "Facade" to describe what GoF would call "Adapter". This matches my impression that the word has become a bit more general. I can also find usage to describe what GoF would call "Decorator" though, and this doesn't match my understanding of general usage (specifically that in a facade the interface is changed, not identical). I can also find plenty of usage that matches the GoF design pattern.
Personally, the word "view" in software is strongly coupled in my mind with "something the user of the application will observe". The word "facade" in software is coupled in my mind with object interfaces (whether they are simplifying, adapting, or loosening the coupling with an external dependency).
For a concrete example of where the conflation of "view" might matter - in the angular compiler there is code dealing with the "view"s that are observed by users of the application, and there is code dealing with the "classes", and "interfaces" in the implementation of the application. Adding code that may need to deal with the "view"s in the implementation of the application has a strong chance at leading to confusion.
Personally, the word "view" in software is strongly coupled in my mind with "something the user of the application will observe".
The first time I read the views proposal, I related the word with SQL Views which made much sense. I'm SQL, a view is like an abstraction over one (or more) table(s) to convey information in a specific [sic] view.
I don't think the word "view" is as coupled with UI as some think.
It feels off to me to name this explicitly based on the design pattern. If we think this is the best keyword, then fine (it definitely doesn't resonate with me), but it feels quite odd to say "here's a feature, which is for some things, but which isn't really what we propose you use for facades (in the design pattern sense), but we're going to call it facade
, (in the design pattern sense) because we think you will be reminded of facades (in the design pattern sense) but you probably don't want to use them for general facades (in the design pattern sense) because they're really something kind of different".
In other words, we seem to feel that even though view
is an accurate analogy, we are concerned that people will confuse them with views
in a design pattern sense (MVC), so instead we're proposing to name them after facades
in a design pattern sense? This all seems off to me.
It feels off to me to name this explicitly based on the design pattern.
I don't know if "facade" is any more explicitly based on "facade design pattern" than "view" is explicitly based on "model view controller". It's entirely plausible that "facade" is inextricably linked to "facade design pattern" for enough readers to make it a not viable - the comments in this thread certainly point that way. If the argument against "view" is that the term is overloaded, it may be that the argument against "facade" is that we'd be contributing to overloading the term.
In the end, I don't think it will matter much which we choose. I'll quickly adapt to thinking of "view" in this sense. The fact that the language we use around member access is similar - the "visibility" of a method - should make it easy.
I think we should keep in mind that the associations we worry about (e.g., that the word 'view' may be understood as a reference to model/view/controller and UI coding; or that 'facade' may be understood as a reference to a GoF design pattern) are extremely transient.
If you actually get the relevant information when needed (e.g., in a Medium blog post when the feature is released, or from a co-worker if you encounter it later on, and haven't read the blog post) then it takes about one minute to learn enough about the feature to understand that those associations aren't helpful (e.g., this is a language mechanism, not a UI class, not a design pattern).
From that point and onwards, I think it's more important that the chosen terminology is informative, that is, that it has the potential to bring up a relevant and useful mental framing of the feature. For instance:
I think both of these work quite well, and I'm sure every developer can stop going down the path of an irrelevant association one minute after they learned that "it's not about MVC and UI coding" or "it's not about the design pattern".
On the other hand, I tend to consider a word like, for example, typedef
as misleading: It seems to imply that the feature introduces a type alias rather than a separate type, and it doesn't bring up anything in my head which is similar to "viewing an existing object as having a different interface" or "applying a facade to an existing object".
It feels off to me to name this explicitly based on the design pattern.
I don't know if anyone was suggesting that. My point was that the definition of "facade" (a new face in front of an existing architectural front that isn't structurally whole by itself) was a fairly good description of this feature (a new interface in front of an existing concrete type that is a leaky abstraction over the original type). The "facade" design pattern is to me a convergent evolution use of the term.
I think we should keep in mind that the associations we worry about (e.g., that the word 'view' may be understood as a reference to model/view/controller and UI coding; or that 'facade' may be understood as a reference to a GoF design pattern) are extremely transient.
In that case, then why not pick an entirely new unused word? Users will learn it whatever word we pick, but I think we do believe that words matter when it comes to learning.
Either way, what isn't transient is tooling. Most syntax highlighters I've seen treat contextual keywords as if they were fully reserved words, so if we pick view
, it's likely going to show up as a keyword in everyone's editor henceforth and forever. That doesn't sound like a great UX to me given that Flutter has a property named view
and may end up having a class named View
(instances of which would likely be stored in variables named view
).
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":