Skip to content

Make it safer to "enter" a (plain) view or an extension type #1665

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
eernstg opened this issue Jun 4, 2021 · 5 comments
Open

Make it safer to "enter" a (plain) view or an extension type #1665

eernstg opened this issue Jun 4, 2021 · 5 comments
Labels
extension-types-later Issues about extension types for later consideration

Comments

@eernstg
Copy link
Member

eernstg commented Jun 4, 2021

This issue proposes a rather mild increase in the protection of views and extension types: We can require that a constructor is called in order to turn an expression of the on-type into an expression of the view/extension type, and still enable higher order cases (such as lists and functions) to allow assignments without a cast.

This proposal about views and this proposal about extension types allow us to make the on-type a subtype of the view or extension type (I'll mention views here, but view could be replaced by extension type everywhere):

view IdNumber on int {} // No members declared, this topic is not about them.
view ProductNumber on int {}

void main() {
  IdNumber id = 24779; // OK, this is an upcast.
  int i = id; // Error, this is a downcast.
  i = id as int; // OK.
  ProductNumber pn = id; // Error, these types are unrelated.
}

This ensures that different views on the same kind of object are not mutually assignable, so we get an error if we assign id to pn. This is quite useful in the situation where we want to use a given representation (perhaps a very lightweight one like int) in several different ways that may need to be kept separate according to the application logic.

However, this subtype based approach provides no checks on the introduction of new values of the given view type, we're always allowed to assign a given int to any of those ...Number view types:

view AgeInYears on int {}
view WeightInPounds on int {}

class Person {
  String name;
  AgeInYears age;
  WeightInPounds ​weight;
  Person(this.name, this.age, this.weight);
}

void main() {
  Person('John Doe', 239, 74); // Oops, should have been `74, 239`!
}

There is no compile-time error in main above, and no run-time error, it is a logical error because those two numbers were intended to be passed in the opposite order. So the program has a bug even though it might not crash.

In order to improve on the correctness support at the point where instances typed as a view type are obtained, we could add a simple rule to the above mentioned proposals:

Assignability is extended by an extra check: A type T is assignable to a type S if

  • T is dynamic, or
  • T <: S and S is not a view type with a conversion constructor, or
  • T <: S and S <: T.

A conversion constructor is a view constructor with the same name as the view that takes a single, mandatory, positional argument whose type is the on-type of the view.

This means that if we declare a conversion constructor in a view then it is required that we call this constructor in the case where we wish to use an expression whose type is the on-type where the view type is expected.

For example:

view AgeInYears on int {
  factory AgeInYears(int value) {
    assert(value < 201);
    return value;
  }
}
view WeightInPounds on int {
  factory WeightInPounds(int value) => value;
}

class Person {
  String name;
  AgeInYears age;
  WeightInPounds ​weight;
  Person(this.name, this.age, this.weight);
}

void main() {
  Person('John Doe', WeightInPounds(239), AgeInYears(74)); // Compile-time error.
}

Note that we can of course use normal abstraction on top of the conversion constructor, if we wish to make things look a bit differently:

extension on int {
  AgeInYears get ageInYears => AgeInYears(this);
  WeightInPounds get weightInPounds => WeightInPounds(this);
}

...

void main() {
  ...
  Person('John Doe', 74.ageInYears, 239.weightInPounds); // OK.
}

The reason why it could be useful to change assignability and keep the subtype relationship from the on-type to the view type is that this allows us to transparently enable adoption of the view on composite entities. For example, we can create a List<int> in some context (where IdNumber is not available), and then we can work with that list (there's no need to copy it) as a List<IdNumber>:

view IdNumber on int {
  factory IdNumber(int i) => i;
}

void main() {
  List<int> xs = [24779, 24780, 24781];
  List<IdNumber> ids = xs; // OK, this is still an upcast.
}

PS: In some situations we might wish to take away the assignability in the higher-order cases, too. In order to achieve that we could use something like a closed view (mentioned in the view proposal), so we can make this distinction a choice that developers can make. Example:

closed view IdNumber on int { // `IdNumber` and `int` are statically unrelated types.
  factory IdNumber(int i) => i;
}

void main() {
  List<int> xs = [24779, 24780, 24781];
  List<IdNumber> ids = xs; // Error, `List<int>` and `List<IdNumber>` are unrelated.
  ids = xs.map((x) => IdNumber(x)).toList(); // The safe way to do it: Create a new list.
  ids = xs as List<IdNumber>; // This will actually succeed at run-time.
}

The cast as List<IdNumber> will succeed at run time (assuming the current proposal), because every view type is reified as the corresponding on-type at run time. However, it is statically unsafe in the sense that we could as well have made xs a Set<int> (or a String, or anything at all), and in that case xs as List<IdNumber> would throw at run time even though there is no compile-time error. This is a major reason why we might want to offer the mild protection described in this issue.

@eernstg eernstg added question Further information is requested extension-types views and removed question Further information is requested labels Jun 4, 2021
@eernstg
Copy link
Member Author

eernstg commented Jun 7, 2021

Making the view type closed by default is certainly possible. That's a question about the likely frequency of use (the most commonly used form should have the most convenient syntax), but also (slightly surreptitiously ;-) as a kind of built-in style guide in case one form is considered more beneficial in terms of software engineering properties: Readability, maintainability, extensibility, and so on.

Reifying the closed view type was my first proposal, but we're gravitating towards a model which is consistently non-reified, as you might well expect from a language mechanism aimed at zero cost.

The main difficulty with the reified design is that if the view type is reified as a type argument (and, most likely, as a type literal) as a value distinct from the on-type, then it seems reasonable (to me) that the associated dynamic checks should be specific to the view as well. For example:

closed view IdNumber on int {}

void main() {
  var ids = [IdNumber(24799)];
  List<Object?> xs = ids;
  xs.add(-1); // Is it good enough that we just check `-1 is int`?
}

If a List<IdNumber> is distinct from a List<int> at run time then we ought to check that each newly added element is an IdNumber. But the most natural way to verify this is to run user-written code that checks whatever is required for the int to be considered an IdNumber (that's the view member that I called verifyThis), and it has serious implications for the language as a whole if we make it a part of dynamic type checks that they could run user-written code. In particular, a substantial number of optimization opportunities could be eliminated.

So the core consideration here is the following: If you can't ensure at run time that a List<IdNumber> contains anything other than plain ints, why would you then give it a representation which is distinct from List<int>? The non-reified model is at least honest, keeping the guarantees and the run-time representation consistent.

@eernstg
Copy link
Member Author

eernstg commented Jun 7, 2021

View types will definitely be allowed as type arguments in a number of cases.

Consider an open view. For this kind of view, the view type and the corresponding on-type are mutual subtypes. It is considered safe to switch between the on-type and the view type at any time (if that's not true then the declaration should not use open), so we'll simply use one or the other type, depending on what's more convenient. For this kind of view it's certainly possible that we'd want to swap the view interface for the on-type interface or vice versa, also when we're working with a higher-order entity like a data structure or a function. So open view types should certainly be allowed as type arguments.

For the plain views (where the declaration just starts with view, without open or closed), we have the subtype relationship, and the intention is that it should be allowed to work on a higher-order entity involving the on-type as if it had used the view type. In other words, we should be able to "apply" the view to existing objects, both stand-alone objects and lists of objects or objects returned by functions, etc. This means that we should support types like List<V> where V is a view type, even though this kind of view doesn't allow us to switch back to List<T> where T is the corresponding on-type, unless we write an explicit downcast.

With closed views, we could say that they cannot be used as type arguments. After all, a closed view is intended to put up barriers such that we don't switch from the on-type to the view type and vice versa lightly. I suspect that it would be far too restrictive to make it an error to use a closed view type as a type argument, but it is definitely a possibility.

Similarly, I suspect that it's a good idea to keep views strictly as a compile-time mechanism (in particular, even closed view types will be reified at run time as the corresponding on-type).

This means that we can use an explicit cast to break the abstraction.

If you want to protect the abstraction then you can make the choice to allocate a wrapper object rather than using a static mechanism, that is, you can use box, and then you'll get the protection that a real object offers. (Of course, you could still have aliases to some underlying on-type instances, as we discussed earlier, but this will give you the same level of encapsulation as the rest of the language). You can of course also use the wrapper class as a type argument.

In summary, I tend to think that we shouldn't make it an error to use a view type as a type argument, for somewhat different reasons in each case.

@eernstg
Copy link
Member Author

eernstg commented Jun 8, 2021

It wouldn't be hard, technically, to provide a separate variant of view types that are reified as type arguments. But you're probably right that it could be a hard sell ("we have plenty of view constructs already, why do you need yet another kind?").

But fat pointers is a whole other story, that's about a construct which has a run-time representation associated with the individual underlying object. A fat pointer is basically a compact implementation of a special kind of wrapper object (the wrapper has methods and access to the wrapped object, but no other state (and no mutable state), so we can use the wrapped object as the identity, and hence we don't need to worry about identity confusion). That's a very interesting topic as well, but quite different from views and zero-cost abstraction!

@sigmundch
Copy link
Member

I do believe this safer explicit conversion mechanism is important and has valuable uses.

Consider for example how eons ago GWT reduced XSS vulnerabilities. They disallowed the equivalent of a set innerHTML(String) API, and replaced it with a set safeHtml(SafeHTML) API. SafeHtml was a wrapper around a String that was guaranteed to be safe by construction. All constructors either validated or sanitized the input, and unsafe constructors (needed for example for data that was pre-sanitized server-side) were scrutinized in detail by security reviewers.

SafeHtml was an explicit wrapper, but I believe it could potentially be implemented as a zero-cost view given sufficient static guarantees to ensure that no unsanitized string can flow unintentionally to a variable of that type.

@eernstg eernstg added extension-types-later Issues about extension types for later consideration and removed extension-types labels Oct 17, 2023
@eernstg
Copy link
Member Author

eernstg commented Oct 17, 2023

Changed labels to reflect the fact that the proposals in this issue will not be part of the upcoming extension types feature, but they could be considered later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
extension-types-later Issues about extension types for later consideration
Projects
None yet
Development

No branches or pull requests

3 participants