-
Notifications
You must be signed in to change notification settings - Fork 213
Add initial draft of static immutability #126
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
Conversation
class Value<T> extends Scalar<T> implements Constant is immutable
where T is immutable {
} would this support |
Question: Do we need additional syntax for the case where a static type context | ||
is not required? | ||
|
||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, please! immutability is really useful for collections outside of supporting issolates, etc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, but if we get the semantics wrong we may find ourselves trying to retrofit immutables onto isolates. So I really prefer that we consider isolates immediately.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. Just saying that it's nice on its own, too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a difference between shallowly unmodifiable and deeply immutable objects, and we probably want both.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does shallow immutability give you that we don't already have with final fields?
## Javascript | ||
|
||
Currently, isolates are not supported in Javascript. If we revisit that, we are | ||
unlikely to be able to support this in full on the web. It is possible that we |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume that all Immutable
types will work fine in JS – just the isolate APIs won't be available.
I'm sure there will be places where we can generate better JS if we know things are immutable, though – so I still consider this a "feature" – even for web-only code.
CC @rakudrama @jmesserly for comments...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it's the communication that wouldn't work out of the box.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think cloning objects should be a sufficient polyfill. You don't get the performance benefits from memory sharing, but you still get the benefit of a second isolate (i.e. web worker).
Not without explicitly adding support in some way. This may be another argument for using a marker interface instead of constraints. |
I really like the semantics this proposal is going for, and I'm hopeful that the GC and isolate communication are solvable issues. Overall, very excited about this proposal! CommentsI'm worried about the syntactic punishment we'd be subjecting the users of immutable classes and collections. It is an especially strange compromise to make given that the proposal requires a language change. I'd imagine that with a language change we could offer a much better syntax for this. A few suggestions:
("value" is also a good option; despite the extra character it is easier to type than "data", which is typed using one hand on a qwerty keyboard) BEFORE: class Value<T> extends Scalar<T> implements Constant is immutable
where T is immutable {
}
foo<S, T, where T is immutable>(Value<T> v) {
}
ImmutableList;
ImmutableSet;
ImmutableMap; AFTER: data Value<data T> extends Scalar<T> {
}
foo<S, data T>(Value<T> v) {
}
DataList;
DataMap;
DataSet; For better heap semantics consider also:
Why Why non-nullability? This is to reduce GC and canonicalization pressure. For example, non-null |
I tend to agree with @yjbanov (I guess that isn't news). Some other thoughts:
void example(List<Player> players) {
if (players is Immutable) {
// ??? Why
}
}
void insertTopPlayer(List<Player> players) {}
void main() {
ImmutableList<Player> p = [Player('Leaf')];
insertTopPlayer(p); // Statically OK, runtime OK, until "<List>.add" is called.
} |
I was thinking along these lines last night, I think it could be a good idea. I do think you want to be able to subclass/implement regular classes from data classes, subject to appropriate constraints, but I think that fits with this.
Do you mean by the programmer, or by the system? This spec doesn't provide for the system to do that - we'd need to explicitly call out different identity semantics for immutable classes. |
As I've specified it, you can only
Me too.. but my guess is that the ability to reuse all of the existing read only APIs that expect |
@leafpetersen: Thanks!
Doesn't that run into the same problem, more or less, as my up-casting issue? import 'animal.dart' show Animal;
data ImmutableAnimal implements Animal { ... } I can see some value in this, for compatibility. I wonder if it is worth it. Are there concrete cases you think implementing non-immutable/value objects buys users a lot?
Since this is, currently, a VM only detail, is it important to make that user-visible? That is, can the VM check (through whatever mechanism it deems best) that objects are transferable, similar to how the V8/JS VM works, and not need a specific type signature? This is a pretty advanced feature (all things considered), so I imagine: abstract class SendPort {
void share(Object transferable);
} ... is fine. You lose (some) static checks, sure. I imagine if desired,
As you can probably imagine, @srawlins @yjbanov and others chatted about this at lunch 😄. The closest comparison I have right now is @davidmorgan's abstract class ImmutableList<T> implements Iterable<T> {
/// Returns a read-only [List] view into the underlying collection.
///
/// Unlike [ImmutableList], this interface is compatible with APIs that expect [List]. Any operation that
/// attempts to write (for example, [List.add]) will throw an [UnsupportedError]. See [UnmodifiableList].
UnmodifiableList<T> asList();
} I really would like to avoid seeing a lot of user code like this: void useList(List<String> names) {
if (names is ImmutableList<String>) {
names = List.of(names);
}
// Add items to names and use it.
} |
Also, if data MatButtonProps {
String title;
bool isRaised;
}
void checkProps(MatButtonProps a, MatButtonProps b) {
if (a != b) {
updateProps(b);
}
} |
Meta-comment: there's a lot of great feedback here - but I'd suggest that anything that isn't specifically actionable on this text (as in stuff I should fix before landing this draft) would be better put here since PR comments tend to disappear into the ether as soon the PR lands. |
Yes. My sense is that, if you don't want to implement a mutable interface with your immutable class, then... don't. :) But the flexibility might be worth it. On the other hand, @munificent points out that this makes it breaking to add a private mutable field to a class (since you might be subclassed by an immutable class). So there is a tradeoff there - I'm open to being convinced.
That's good input, thanks. One question that feeds into this is whether the ability to imperatively initialize immutable lists is important. I specified it that way based on experiences in other languages with immutable vectors, where you sometimes have to jump through hoops to efficiently create an immutable array. If we want that ability, then there needs to be some mutation API, and there's some convenience in re-using the List API. |
I'll try :). One nice thing here is we don't need language features do to some usability features on how an
No thank you for starting this discussion!
I think it's cool. I don't know if its important. It's also not clear to me if this could be a future enhancement (i.e. get declarative and-or explicit conversion from At least for the Flutter case, changing Having
I guess another option is some sort of generated builder and/or |
Re: ImmutableList implements List I don't think we should keep backwards compatibility by making static errors into runtime errors. If I'm a user and I'm not sure whether I can use an ImmutableList with an API, I have to either look at its implementation or copy. The current UnmodifiableListView and friends already accomplish this, and they are seldom used. An alternative: make the UnsupportedError a NoSuchMethodError
If using immutable collections in Flutter ends up being valuable by catching bugs or leading users to better patterns, we could experiment with a Flutter-specific lint. In the non-flutter case, types can be gradually widened as needed and where possible. I think the benefit of being able to immediately pass an ImmutableList to interfaces accepting Lists would lose out in the long-term to more predictable collection behavior. |
I'll add some comments over on the issue. |
This is definitely possible, but it has the following issues:
|
I added some notes based on feedback so far. I'm going to land this doc, and will iterate further based on comments. Let's take discussion here. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd consider a more Dartino-like approach where we don't introduce new nominal types, but allow all classes to create immutable objects.
Say: Any object with only final fields and where all fields hold immutable objects, is itself immutable. Maybe only if created using a const constructor.
We can then add immutability to the type system as a type modified: const T
is a sub-type of T
and of T<:S
then const T <: const S
. It gives a parallel type hierarchy (like non-nullability). Maybe "const" is not the right word, because it allows non-const values, as long as they are immutable (const is just a way to create and canonicalize some immutable objects at compile-time).
Maybe we can even define classes that are only immutable: const class FinalWord { final String word; const FinalWord(this.word); }
. That type only exists as immutable, there is no "mutable" superclass of it.
|
||
The SendPort class is extended with a new method `void share<T, where T is | ||
immutable>(T message)` which given a reference to an immutable object graph, | ||
shares that reference with all receivers of the SentPort. Note that the object |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does "shares" mean?
Is it sent on the port, or is it just prepared for sending.
What class does the object have on the "other side"?
Can it access static state? Other types? Can an immutable object have a method like:
foo() => new _Foo(_staticCounter++);
If the receiving isolate has not imported the library declaring the class of the object, can you send it? If you do, what is its run-time type? Which static state can it access?
If the receiving isolate has imported the same library, what is the run-time type of the received object? We have two different instances of the library (they have different static state), so which static state will the object access?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is sent on the port.
I would expect that it can access static state, and every isolate has its own instance of the static state.
Broadly, to your questions, I would expect isolates to share the same code, hence have the same classes loaded, etc. This is what the (very minimal, vague) docs for Isolate.spawn seem to describe? If there are other ways to create isolates that don't share this property, I would suggest that they not be able to access the shared heap.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can use Isolate.spawnUri
to run arbitrary code that doesn't need to have anything in common with the spawning isolate. You can usually only send JSON-like structures between such isolates.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See dart-lang/sdk#36648 .
in a modifiable state. Mutation operations may be performed on such an instance | ||
up until the first point at which the instance escapes (that is, is captured by | ||
a closure, is assigned to another variable or setter, or is passed as a | ||
parameter). It is a static error if a mutation operation is performed on an |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So we have "mutate until frozen" collections.
Do we allow that for other, user written, immutable types too?
(We can perhaps get away with this by saying that you can only mutate the object using cascades on the object creation expression, and that mutating methods on an immutable object may not leak this
in any way. Still sounds complicated).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I raised this question below - if there's need for it we can do it. It's basically a limited use of typestate. How limited depends on how much power we need or don't need.
always initialized in an umodifiable state. | ||
|
||
Question: Is this functionality needed? With spread collections, many patterns | ||
will be expressible directly as a literal. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Likely not needed, and it's a very fragile functionality, so I'd prefer to avoid it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe. @munificent agrees with you. After years of struggling with vector initialization in functional languages.... I'm a little more skeptical.
### Alternative collection approach | ||
|
||
Instead of making `ImmutableList` a subtype of `List`, we could make it either | ||
an unrelated type, or a supertype of `List`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As you say below, it can't be a supertype if subtypes of immutable types must be immutable. If that isn't true, then the type system won't be the thing enforcing immutability.
(It's also a good reason why you should never name a type negatively, because a restriction is usually something you can remove in a sub-class, and new MutableList() is ImmutableList
being true is just weird).
We can add a supertype of List
called Sequence
, and then let ImmutableList
implement Sequence
. We probably won't because it introduces a new super-type of List
that code authors should then change all their read-only List
accepting functions to using.
So ImmutableList
shoud either be a subtype of List
or be unrelated to List
.
The former allows it to be used with code that currently accepts a list.
The latter allows it to have a different API, say with functional updates. I don't think introducing a completely new API is worth it.
the case of collections). Alternatively, we could simply not enforce deep | ||
immutability statically, and instead dynamically traverse an object grap before | ||
sharing it to check for immutability. This is expensive, but perhaps marginally | ||
less so than copying. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Dartino approach was to have one bit on each object telling whether it's immutable.
(That could also be based on which GC page it is allocated in, or something similar).
When you create a new object using a const constructor, or an .unmodifiable
constructor of a system collection, then the bit gets set if all the fields/elements/entries are immutable.
You never need to do a deep check, but you do one more computation for each component object when creating a composite object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I thought about writing this up as an alternative proposal, may still do (or feel free). I'm not very enthusiastic about it though. It makes every allocation in the program a little slower (or potentially a lot slower in JS), even if no immutability is used.
with this. | ||
|
||
A benefit of this is that changing APIs (especially Flutter APIs) to take | ||
`ImmutableList` as an argument would be non-breaking. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not clear how this would work. A type annotation of ImmutableList
should accept the subtype _GrowableList
.
If it's not type based, then it should probably not show up in the type system at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I don't see a coherent story here, but added it based on discussion for future reference.
I'm open to this, but I'd like to see it worked out. The key point of what I'm specifying here is that immutable objects can be allocated directly into a shared heap. You can instead copy into a shared heap, or do something else, but what that something else is needs to be worked through.
const is certainly not enough.
Maybe? I don't know what this means though. What are the inhabitants of I would like to see other proposals worked up (I may try to write up the dynamic version) - this is still very much exploratory. |
@lrhn, the Dartino approach has some theoretical appeal in that you are kind of like composing immutability with existing concepts. However, I think there are downsides. This proposal is about sharing memory and memory implies data and not behavior. Existing class semantics are great at expressing behaviors, such as services, So in practice you will rarely find it useful to take any random class and use it in two modes, a mutable non-shared mode and immutable shared mode. Instead you are more likely to design the class for one use-case or the other. I list some of the desired properties of immutable objects in #125 (comment), and I do not see a clear path there if we start with existing classes. However, I'd still be curious to see a complete proposal. |
cc @aam @mraleph @munificent @lrhn for comment.