-
Notifications
You must be signed in to change notification settings - Fork 23
make UnmodifiableSetView
compile time unmodifiable
#853
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
Comments
This is an intentional design choice, granted an unfortunate one for the reasons you describe, but it is necessary in order to implement the A different choice could have been made to only implement the With the knowledge of those two options, I would personally suggest the following, based on your use case:
|
Can we improve this? @jakemac53 What is your opinion on defining a |
The use-case for
If you know and trust that the code you pass your set to won't mutate that set, you can just pass the set itself. If you don't trust that, and want to be safe, you can wrap it as an
Not without significant changes to ... almost everything. A set with no mutating operations is not the To add a non-mutable superclass of
(They're the same type hierarchy, the only distinction is what existing code using the word In either case, we'd have to say which type a set literal creates. A Removing members from Adding a non-mutable superclass would not break existing code. If that's what we wanted, I wouldn’t start from here. There is really no chance of introducing unmodifiable superclasses of collections today and getting them into common use any time soon. If the unmodifiable class is a superclass, then you can't use types to ensure that you don't get a mutable set. We could introduce such unmodifiable super-classes, not force any use, and see if people would start using them. That would fail right out of the door because people won't be able to use an instance of the superclass for much unless the platform libraries will accept it as an argument. The Should there be unmodifiable typed-data lists? There should be Etc. So, not easy. Also not a big benefit to the general user. If you keep using the mutable version everywhere, your code will work. Now we have extra complexity in API design, extra complexity in API implementation, the possibility of two APIs not being very compatible because one uses I do understand the drive to make every property of an object into a property of the type, so that static type checking can prevent runtime errors, The tradeoff that Dart has chosen, and it was done very early in the language and platform library design when the language was much more dynamic and type-unsound, was that using one (potentially-)mutable type for each of The added complexity of having multiple versions of each would not pay for itself in benefits for actual users. Even in today's more soundly typed language, I actually believe that tradeoff is still correct. It's incredibly rare that a function mutates a collection that it gets as argument, to the point where if it does so, TL;DR: An unmodifiable collection should be a supertype of the mutable collection, otherwise API inheritance wont work. It won't be named One thing Dart could do is to introduce marker interfaces; abstract interface class UnmodifiableCollection {}
abstract interface class FixedLengthList {} and make every unmodifiable collection implement the former, and fixed-length lists implement the latter. |
@lrhn thanks for your really detail answer.
What about having it separate? The use case I describe is, "give me a compile error if I call
if the function wraps the collection given as an argument in |
I'd just cast the set to You can't use |
@lrhn I changed a lot of type in my codebase for iterable as parameters type instead of set or list and I'm 99% already there. |
This is an amazing analysis, @lrhn! Among other things, it illustrates how daunting the request of this issue is: The most important observation is probably the following:
This is the main reason why the proposal below most likely won't be very disruptive. Dividing collection types into mutable and non-mutable onesAssuming that this statement about being 'incredibly rare' is true, along with an assumption that the amount of code that needs to mutate a collection is generally much smaller than the amount of code that just needs read-only access to it, we could consider using the following approach:
The point is that we can now separate the two cases clearly, with support from the type checker:
The invariance property provides a compile-time guarantee that the mutating operations (like Conversely, the types we're using today are always unsafe with respect to mutating members. For example, Note that Also note that this distinction is different from the distinction between modifiable and non-modifiable collections. It is possible, and perhaps even useful (depending on the run-time cost associated with copying a potentially large number of collections) to pass a non-modifiable collection in each case where a collection is passed with a covariant type (like However, if the protection offered by static typing is considered sufficient then there's no need to create new copies of various collection objects just so they can be unmodifiable. Presumably, non-modifiable collections will be used in fewer cases than today: It is the only kind of protection against mutation we have today; when Apart from the compile-time/run-time difference between the proposal in this comment and the use of unmodifiable collections, they also differ in that unmodifiability is an inherent property of a collection object whereas the protection based on the distinction between types like Literal typesAll collection literals will have invariant static types: However, MigrationThis approach does not give rise to any changes to the declarations of collection types. This implies that all the code that works fine today can just be left unchanged. Hence, migration can be done incrementally. If a library L is selected for migration to a more strict handling of collection mutability, the lint If there is one or more messages from This may turn out to be a bug, which is good: You just found it. Otherwise, the collection is really intended to be mutated at this point, so the static type of the collection should be made invariant. You can use standard techniques to enable mutating access to some amount of code, and non-mutating access to other parts of the code. // Before.
class A {
final List<num> xs = [];
void foo() => xs.add(1.5); // Lint!
}
// After.
class A {
final List<exactly num> xs = [];
void foo() => xs.add(1.5); // OK.
}
// Alternative after, restricting mutating access to the list to the current library.
class A {
final List<exactly num> _xs = []; // Mutating access granted privately.
List<num> get xs => _xs; // Everybody else just gets the less-privileged `List<num>`.
void foo() => _xs.add(1.5); // OK.
} If some flagged mutation is performed on an imported entity The maintainers of L2 may also respond that they aren't ready to migrate their code in the near future. In this case you can at least change your code such that it doesn't get flagged by the lint, and such that there will not be any run-time type errors: import 'l2.dart'; // Which has `List<num> foreignList = ...;`
// Before.
void foo() {
foreignList.add(1.5); // Lint!
}
// After, if `l2.dart` won't be migrated any time soon.
void foo() {
var xs = foreignList; // Create a local variable: Enable promotion.
if (xs is List<exactly num>) {
xs.add(1.5); // OK.
} else {
... // Error handling, presumably more graceful than throwing a `TypeError`.
}
} Steady stateIt would be possible to migrate all Dart code to use the style where mutability of collections is expressed explicitly everywhere. However, it seems likely that this will not happen. The reason is that the dynamically checked covariance and the freely available access to mutating members that shouldn't be used don't create a huge amount of problems (apparently), and developers have lots of other things to do. Perhaps the approach described here will only be used in code which is particularly critical (because it's used in a situation where a bug could cause serious real-world damage, or because the code is very widely used, and even smaller improvements of the overall correctness would affect a lot of people). This implies that we're likely to have a situation where some parts of a system are migrated to use this approach, and other parts are not. This could create some readability issues at the boundary. For example, Why not generalize?It could be claimed that if this approach is any good for collections then it should also be used for any other class which has unsafe mutating methods. The lint This would be possible, but collections are special in that they are used so pervasively, and they have literal forms, and it's such a daunting task to handle the issue at the root cause (which is that the type parameters weren't declared as invariant from day one). There may be other classes that would benefit from a similar treatment as the collection classes. However, in general, other classes are much more amenable to other strategies, for instance, changing the declaration to use declaration-site invariance. |
UnmodifiableSetView
only offers immutability by throwing exceptions on inappropriate function calls.These checks are done at runtime, and my code can call
add
all day long; nothing will complain until I run the program.UnmodifiableSetView
or any alternative should remove these functions from the interface and provide compile time checks. my code should not be allowed to callUnmodifiableSetView.add
because it should not exits in the first place.The text was updated successfully, but these errors were encountered: