-
Notifications
You must be signed in to change notification settings - Fork 205
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
proposal: immutable types - take 1 #225
Comments
@tatumizer wrote:
I just wanted to make sure that we don't have a lot of threads on similar topics without creating some connections between them. The point is that we should avoid too much redundancy, and we should be aware of possible synergies, but I wasn't implying that these particular topics would necessarily be merged in any specific manner, or at all! ;-) |
I'm afraid if we add language features to satisfy one use-case at a time we will quickly find ourselves with a very big language with features doing same things slightly differently, i.e. a very confusing one. I think #125 is a good starting point to discuss how it can cover as many use-cases as possible without making the proposal unwieldy. BTW, sharing data across isolates is not as different from the Flutter mutation issue (dart-lang/sdk#27755) as it may seem. In both cases the fundamental issue is concurrent modification of shared data. |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Frozen-ability is a useful feature, it could allow to convert any object that has non final members into a final one without requiring the creation of an additional special object. If this was incorporated in the VM as a flag, then they would be efficient. What I think is wrong is creating frozen-able objects by default. Imagine objects can be declared like this: var x = ^Foo() // creates a frozenable object
x = ^^Foo() // creates an already frozen object (immutable) All objects by default can be declared in two versions, regular one and frozen-able ones. Then the feature becomes innocuous, whenever someone sees a declaration of an object with ^, they would go check the docs for the meaning of it. It must somehow be made easy to search, because it's non trivial to search for symbols in google or in the language tour (a word is direct to find). Immutable would then become an extension of Fronzenable and Flutter could receive Frozenable objects. It's very easy then for the users to declare children lists, all that is requires is to prefix a ^ in the list declaration. This is also useful for this idea in #125, it makes it easy to create sealed type var from any object where all their members are exposed instead of only the final members. By default if a reference is used in a sealed var If that feature existed and experience showed that in practice most people prefers frozen-able objects, then it could be decided to default objects to frozen-able types and create traditional object using a special notation (revert the situation). |
It is always an enhancement to have frozenability in objects. But it must be a hidden interface from the default Object one, that’s why I couldn’t help but think it’s wrong. Skip this section until end skip mark, deliberation about the logic of wrong Types are the most important part of the documentation of a program. They express the interface that is going to be used for a given variable. This interface is specially relevant for mutation operations, but not so much for read-only which is innocuous. If a function receives a List, then it is expressing it can perform mutations on the list, because if it weren’t, then it would request for a ListView. It’s undeniable that this expressiveness is important information, it allows the user of the API to make assumptions about guaranteed behavior of the program without having to peek into the documentation or into the function body. If the program fails for any reason, it’s preemptively known that if a function received a ListView the problem could not be that the List was unexpectedly mutated inside the function. This is the great advantage of programming through interfaces, it is possible to infer a lot of behavior without reading a single line of documentation. On the same lines, if a function pretended to mutate an object by freezing it, then it’s undeniable that it would be beneficial that the function was explicit about this intention which means it should receive the frozen-able version of the type instead of the regular one. Under the context where it is accepted that static explicitness is the correct way to define functions when it comes to mutations, general frozenability in the standard object interface is wrong, because it is by default passing a mutation operation that is not being used. If most of the time frozenability was being used, then it would be correct to default it and ObjectView could exist to express that the mutable interface of an object is not going to be used. Under the context of dynamic typing where there are no interfaces general frozenability is correct. If all objects are inherently freezable but this was hidden from the default interface, then it’s fine because analyzer warnings could catch whenever an object is being casted to their freezable type. A function that casts an object received as parameter to their freezable type would be a bad defined function in the same way it is wrong to define a Widget with mutable fields. This implies that it is always suggested against casting an object to its freezable type, so in practice a program that follows the instructions should only have freezable types that were defined from their origin to be freezable. But it is positive to leave the door open, because a subset of cases may call for a transgression of this ideal and besides it has the advantage of allowing to directly pass freezable types by using the regular constructor of a class. For example in Flutter placing children like How to make sense of this in the current Dart semantics? Ideas required here, End skip Frozenability should be developed, it’s very useful that functions can declare that they are receiving objects that they pretend to own. These are the use cases of a parameter being passed to a function:
Both cases in the middle are covered by freezable objects. With freezable objects, immutable types become simply an interface, because converting a freezable type into an immutable type is trivial. Freezable is an improvement over immutable because it allows creating immutable types by processing the data directly in the object and not creating a copy at the end. Sometimes the caller of the function might be fine with converting the existing instance into immutable. Freezable is a missing contract in function signatures that has existed in all static typed languages that at least I have used. Perhaps you are right that it could generally be accepted that certain types like List or Map benefit in the majority of the cases from frozenablity. I cannot deny that many times functions receive native data structures as parameters and because it is unknown whether they will keep the reference (just like the Flutter case) I pass a copy just in case. Frozenability disambiguates this, if the function receives ClassicList it means it will mutate and drop the reference, if it receives List (freezable) it should be assumed it will be kept. That situation needs a deep examination. |
This is the sketch of the proposal to introduce immutable types.
This material is based on discussion in dart-lang/sdk#27755
I tried to integrate the ideas from different contributors into a (hopefully) coherent whole, without inventing much of my own.
This design is supposed to be applicable to any type, but to sound less abstract, I will use List as an example.
The idea: introduce 2 operators with the following pseudocode:
In addition, we define View<T> to target use cases not covered by ^. The conjecture is that 3 operations: (^, ^^ and creating a View), used as "building blocks", are enough to cover most (or all?) scenarios and expectations of who can modify what and who can see what changes. (I can't prove the "all" part, it might be even wrong, but I believe we will be closer to the general solution regardless).
Operator ^^ is unusual, it changes the operand, but it's not unprecedented: operator ++x does the same, hence the choice of 2 symbols ^^ to emphasize the analogy.
Immutable type is denoted as ^List.
Let
Then
(*) to make transition of existing code base easier, the rule has to be relaxed: TBD.
Class View<T> is a generalization of UnmodifiableListView, applicable to all classes; view has the same interface as ^T. Used in cases where the callee agrees to not modify the list, but is ready to correctly take into account the changes made by the owner dynamically. The distinction is important enough to warrant a different class. This class is viral, but the use cases for it must be rare.
The design is 1) not viral 2) not breaking
EDIT: interesting math trivia here:
The text was updated successfully, but these errors were encountered: