Skip to content
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

Closed
tatumizer opened this issue Feb 15, 2019 · 10 comments
Closed

proposal: immutable types - take 1 #225

tatumizer opened this issue Feb 15, 2019 · 10 comments

Comments

@tatumizer
Copy link

tatumizer commented Feb 15, 2019

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:

operator ^(list) => list is immutable ? list : createImmutableCopy(list)
operator ^^(list) {
  if (list is mutable) {
    render list immutable (by sealing or somehow);
  }
  return list;
} 

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

  • someList - the list compiler knows nothing about,
  • listKnownToBeImmutable - statically proved to be immutable
  • listKnownToBeMutable - statically proved to be mutable

Then

^List im1 = someList; // statically OK, but may fail in runtime if someList is mutable (see \*)
^List im2 = ^someList; // OK, may create an immutable copy if someList is mutable
^List im3 = ^^someList; // always succeeds, no copies necessary, but someList also becomes immutable
^List im4 = listKnownToBeImmutable; // OK, no copies
^List im5 = ^listKnownToBeImmutable; // OK, no copies, ^ is redundant (warning?)
^List im6 = ^^listKnownToBeImmutable; // OK, no copies, ^^ is redundant (warning?)
^List lm7 = listKnownToBeMutable; // static error (see \*)
^List im8 = ^listKnownToBeMutable; // creates immutable copy
^List im9 = ^^listKnownToBeMutable; // renders operand immutable and assigns to im9

List l1 = someList; // always OK
List l2 = ^someList // like im2 above
List l3 = ^^someList; // like im3 above
// the rest is obvious

(*) 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:

  • ^View<T> = ^T. That is, operation "^" applied to a view can be defined as "copy a snapshot of a View, and get a real ^T".
  • View<^T> = ^T (obviously)
@eernstg
Copy link
Member

eernstg commented Feb 15, 2019

Note the potential overlap with #125 and #117.

@eernstg
Copy link
Member

eernstg commented Feb 18, 2019

@tatumizer wrote:

.. how to proceed.

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! ;-)

@yjbanov
Copy link

yjbanov commented Feb 19, 2019

IMO, #125 is rather complicated, targets different use case, does not necessarily solve the problem in Flutter (the only massive motivating environment we have), viral - in other words, it's not apples-to-apples comparison.

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.

@icatalud

This comment has been minimized.

@icatalud

This comment has been minimized.

@icatalud

This comment has been minimized.

@icatalud

This comment has been minimized.

@icatalud

This comment has been minimized.

@icatalud
Copy link

icatalud commented Jan 4, 2020

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 sealed x = y, it automatically freezes the var (it must be of a frozen-able type or it's a runtime error).

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).

@icatalud
Copy link

icatalud commented Jan 6, 2020

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 children: [w1, w2, ...] is completely possible without adding any extra syntax if the framework received freezable List instead. Developers that defined a regular list and passed it to the children list would get analyzer warnings. That would be consistent.

How to make sense of this in the current Dart semantics? Ideas required here, Object() would have to be a factory constructor that actually instantiates a FrozenableObject, but that makes super() unviable in a class that extends Object unless a concession was made. Perhaps an option could be to transform the default Object constructor into an external constructor and assume that it is hiding the Frozenable interface.

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:

  • Read and dropped (view)
  • Read and kept (immutable)
  • Mutated and kept (freezable)
  • Mutated and dropped (classic)

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants